“ Effective TypeScript探讨了我们在使用 TypeScript 时遇到的最常见问题,并提供实用的、以结果为导向的建议。无论您的 TypeScript 经验水平如何,您都可以从本书中学到一些东西。”
微软 TypeScript 工程主管 Ryan Cavanaugh
“Effective TypeScript explores the most common questions we see when working with TypeScript and provides practical, results-oriented advice. Regardless of your level of TypeScript experience, you can learn something from this book.”
Ryan Cavanaugh, Engineering Lead for TypeScript at Microsoft
“这本书充满了实用的秘诀,必须放在每个专业 TypeScript 开发人员的办公桌上。即使你认为自己已经了解 TypeScript,也可以阅读这本书,你不会后悔的。”
Yakov Fain,Java 冠军
“This book is packed with practical recipes and must be kept on the desk of every professional TypeScript developer. Even if you think you know TypeScript already, get this book and you won’t regret it.”
Yakov Fain, Java Champion
“TypeScript 正在接管开发世界......本书提供的对 TypeScript 的更深入理解将帮助许多开发人员在利用 TypeScript 的强大功能时大放异彩。”
Jason Killian,TypeScript NYC 的联合创始人和前 TSLint 维护者
“TypeScript is taking over the development world...The deeper understanding of TypeScript this book provides will help many developers shine as they take advantage of TypeScript’s powerful features.”
Jason Killian, Cofounder of TypeScript NYC and former TSLint maintainer
“这本书不仅仅是关于 TypeScript 可以做什么——它还教导了为什么每种语言的特性都是有用的,以及在什么地方应用模式以获得最大的效果。本书侧重于对日常工作有用的实用建议,并提供足够的理论让读者深入了解一切是如何运作的。我认为自己是 TypeScript 的高级用户,我从这本书中学到了很多新东西。”
Jesse Hallett,Originate, Inc. 高级软件工程师
“This book is not just about what TypeScript can do—it teaches why each language feature is useful, and where to apply patterns to get the greatest effect. The book focuses on practical advice that will be useful in day-to-day work, with just enough theory to give the reader a deep understanding of how everything works. I consider myself to be an advanced TypeScript user, and I learned a number of new things from this book.”
Jesse Hallett, Senior Software Engineer, Originate, Inc.
改进 TypeScript 的 62 种具体方法
62 Specific Ways to Improve Your TypeScript
版权所有 © 2020 丹·范德卡姆。版权所有。
Copyright © 2020 Dan Vanderkam. All rights reserved.
在美利坚合众国印刷。
Printed in the United States of America.
由O'Reilly Media, Inc. 出版 ,1005 Gravenstein Highway North, Sebastopol, CA 95472。
Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.
购买 O'Reilly 书籍可用于教育、商业或促销用途。大多数书籍还提供在线版本 ( http://oreilly.com )。如需更多信息,请联系我们的企业/机构销售部:800-998-9938 或corporate@oreilly.com。
O’Reilly books may be purchased for educational, business, or sales promotional use. Online editions are also available for most titles (http://oreilly.com). For more information, contact our corporate/institutional sales department: 800-998-9938 or corporate@oreilly.com.
有关发布详细信息,请参阅 http://oreilly.com/catalog/errata.csp?isbn=9781492053743 。
See http://oreilly.com/catalog/errata.csp?isbn=9781492053743 for release details.
O'Reilly 徽标是 O'Reilly Media, Inc. 的注册商标 。Effective TypeScript、封面图片和相关商业外观是 O'Reilly Media, Inc. 的商标。
The O’Reilly logo is a registered trademark of O’Reilly Media, Inc. Effective TypeScript, the cover image, and related trade dress are trademarks of O’Reilly Media, Inc.
本作品所表达的观点是作者的观点,不代表出版者的观点。尽管出版商和作者已尽善意努力确保本作品中包含的信息和说明准确无误,但出版商和作者不对错误或遗漏承担任何责任,包括但不限于对因使用或对这项工作的依赖。使用本作品中包含的信息和说明的风险由您自行承担。如果本作品包含或描述的任何代码样本或其他技术受开源许可或他人知识产权的约束,您有责任确保您对它们的使用符合此类许可和/或权利。
The views expressed in this work are those of the author, and do not represent the publisher’s views. While the publisher and the author have used good faith efforts to ensure that the information and instructions contained in this work are accurate, the publisher and the author disclaim all responsibility for errors or omissions, including without limitation responsibility for damages resulting from the use of or reliance on this work. Use of the information and instructions contained in this work is at your own risk. If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights.
978-1-492-05374-3
978-1-492-05374-3
[大规模集成电路]
[LSI]
对于亚历克斯。
你就是我喜欢的类型。
For Alex.
You’re just my type.
2016 年春天,我去谷歌旧金山办公室拜访了我的老同事 Evan Martin,问他有什么让他兴奋的。多年来我多次问过他同样的问题,因为答案范围广泛且不可预测但总是很有趣:C++ 构建工具、Linux 音频驱动程序、在线填字游戏、emacs 插件。这一次,Evan 对 TypeScript 和 Visual Studio Code 感到兴奋。
In the spring of 2016, I visited my old coworker Evan Martin at Google’s San Francisco office and asked him what he was excited about. I’d asked him this same question many times over the years because the answers were wide-ranging and unpredictable but always interesting: C++ build tools, Linux audio drivers, online crosswords, emacs plugins. This time, Evan was excited about TypeScript and Visual Studio Code.
我很惊讶!以前听说过TypeScript,但只知道它是微软创造的,误以为它与.NET有关。作为一个终生的 Linux 用户,我无法相信 Evan 会跳槽到 Microsoft 团队。
I was surprised! I’d heard of TypeScript before, but I knew only that it was created by Microsoft and that I mistakenly believed it had something to do with .NET. As a lifelong Linux user, I couldn’t believe that Evan had hopped on team Microsoft.
然后 Evan 向我展示了 vscode 和 TypeScript 游乐场,我立即被转换了。一切都那么快,代码智能使得构建类型系统的心智模型变得容易。在 Closure Compiler 的 JSDoc 注释中编写了多年的类型注释之后,这感觉就像真正有效的类型化 JavaScript。微软在 Chromium 之上构建了一个跨平台的文本编辑器?也许这是一种值得学习的语言和工具链。
Then Evan showed me vscode and the TypeScript playground and I was instantly converted. Everything was so fast, and the code intelligence made it easy to build a mental model of the type system. After years of writing type annotations in JSDoc comments for the Closure Compiler, this felt like typed JavaScript that really worked. And Microsoft had built a cross-platform text editor on top of Chromium? Perhaps this was a language and toolchain worth learning.
我最近加入了 Sidewalk Labs,正在编写我们的第一个 JavaScript。代码库仍然很小,Evan 和我能够在接下来的几天内将其全部转换为 TypeScript。
I’d recently joined Sidewalk Labs and was writing our first JavaScript. The codebase was still small enough that Evan and I were able to convert it all to TypeScript over the next few days.
从那以后我就上瘾了。TypeScript 不仅仅是一个类型系统。它还带来了一整套快速且易于使用的语言服务。累积效应是 TypeScript 不仅让 JavaScript 开发更安全:它也让它变得更有趣!
I’ve been hooked ever since. TypeScript is more than just a type system. It also brings a whole suite of language services which are fast and easy to use. The cumulative effect is that TypeScript doesn’t just make JavaScript development safer: it also makes it more fun!
Effective书籍旨在成为其主题的“标准第二本书” 。如果您之前有一些使用 JavaScript 和 TypeScript 的实践经验,您将充分利用Effective TypeScript 。我写这本书的目的不是教你 TypeScript 或 JavaScript,而是帮助你从初级或中级用户晋升为专家。本书中的项目通过帮助您构建 TypeScript 及其生态系统如何工作的心智模型,让您意识到需要避免的陷阱和陷阱,并指导您以尽可能最有效的方式使用 TypeScript 的许多功能来做到这一点。参考书会解释一种语言让你做 X 的五种方法,而一本有效的书会告诉你使用这五种方法中的哪一种以及为什么。
The Effective books are intended to be the “standard second book” on their topic. You’ll get the most out of Effective TypeScript if you have some previous practical experience working with JavaScript and TypeScript. My goal with this book is not to teach you TypeScript or JavaScript but to help you advance from a beginning or intermediate user to an expert. The items in this book do this by helping you build mental models of how TypeScript and its ecosystem work, making you aware of pitfalls and traps to avoid, and by guiding you toward using TypeScript’s many capabilities in the most effective ways possible. Whereas a reference book will explain the five ways that a language lets you do X, an Effective book will tell you which of those five to use and why.
TypeScript 在过去几年里发展迅速,但我希望它已经足够稳定,以至于本书中的内容在未来几年内仍然有效。本书主要关注语言本身,而不是任何框架或构建工具。您不会找到任何示例来说明如何将 React 或 Angular 与 TypeScript 一起使用,或者如何配置 TypeScript 以与 webpack、Babel 或 Rollup 一起使用。本书中的建议应该适用于所有 TypeScript 用户。
TypeScript has evolved rapidly over the past few years, but my hope is that it has stabilized enough that the content in this book will remain valid for years to come. This book focuses primarily on the language itself, rather than any frameworks or build tools. You won’t find any examples of how to use React or Angular with TypeScript, or how to configure TypeScript to work with webpack, Babel, or Rollup. The advice in this book should be relevant to all TypeScript users.
当我刚开始在 Google 工作时,我收到了Effective C++第三版的副本。它不同于我读过的任何其他编程书籍。它没有试图让初学者易于理解,也没有试图成为该语言的完整指南。它没有告诉您 C++ 的不同功能有什么作用,而是告诉您应该如何使用和不应该如何使用它们。它通过许多由具体例子激发的简短、具体的项目来做到这一点。
When I first started working at Google, I was given a copy of the third edition of Effective C++. It was unlike any other programming book I’d read. It made no attempt to be accessible to beginners or to be a complete guide to the language. Rather than telling you what the different features of C++ did, it told you how you should and should not use them. It did so through dozens of short, specific items motivated by concrete examples.
在每天使用该语言的同时阅读所有这些示例的效果是显而易见的。我以前使用过 C++,但这是我第一次对它感到满意并且知道如何考虑它向我提供的选择。在以后的几年里,我在阅读Effective Java和Effective JavaScript时也会有类似的经历。
The effect of reading all these examples while using the language daily was unmistakable. I’d used C++ before, but for the first time I felt comfortable with it and knew how to think about the choices it presented me. In later years I would have similar experiences reading Effective Java and Effective JavaScript.
如果您已经习惯于使用几种不同的编程语言,那么直接潜入新语言的奇怪角落可能是挑战您的心智模型并了解它与众不同之处的有效方法。通过写这本书,我学到了很多关于 TypeScript 的知识。我希望你有同样的阅读体验!
If you’re already comfortable working in a few different programming languages, then diving straight into the odd corners of a new one can be an effective way to challenge your mental models and learn what makes it different. I’ve learned an enormous amount about TypeScript from writing this book. I hope you’ll have the same experience reading it!
本书是“项目”的集合,每个项目都是一篇简短的技术文章,为您提供有关 TypeScript 某些方面的具体建议。这些项目按主题分组到章节中,但您可以随意跳转并阅读您最感兴趣的内容。
This book is a collection of “items,” each of which is a short technical essay that gives you specific advice about some aspect of TypeScript. The items are grouped thematically into chapters, but feel free to jump around and read whichever ones look most interesting to you.
每个项目的标题都传达了关键要点。这些是您在使用 TypeScript 时应该记住的事情,因此值得浏览目录以了解它们。例如,如果你在写文档,并且有一种不应该写类型信息的念头,那么你就会知道去阅读条款 30:不要在文档中重复类型信息。
Each item’s title conveys the key takeaway. These are the things you should remember as you’re using TypeScript, so it’s worth skimming the table of contents to get them in your head. If you’re writing documentation, for example, and have a nagging sense that you shouldn’t be writing type information, then you’ll know to go read Item 30: Don’t repeat type information in documentation.
该项目的文本激发了标题中的建议,并用具体的例子和技术论证来支持它。几乎本书中的每一个要点都通过示例代码进行了演示。我倾向于通过查看示例和略读散文来阅读技术书籍,我假设您也做类似的事情。我希望你能阅读散文和解释!但是,如果您略读示例,您仍然应该能够理解要点。
The text of the item motivates the advice in the title and backs it up with concrete examples and technical arguments. Almost every point made in this book is demonstrated through example code. I tend to read technical books by looking at the examples and skimming the prose, and I assume you do something similar. I hope you’ll read the prose and explanations! But the main points should still come across if you skim the examples.
阅读该项目后,您应该明白为什么它会帮助您更有效地使用 TypeScript。如果它不适用于您的情况,您也会有足够的了解。Effective C++的作者 Scott Meyers给出了一个令人难忘的例子。他遇到了一个编写在导弹上运行的软件的工程师团队。他们知道他们可以忽略他关于防止资源泄漏的建议,因为当导弹击中目标并且他们的硬件爆炸时,他们的程序总是会终止。我不知道有任何具有 JavaScript 运行时的导弹,但 James Webb 太空望远镜有一个,所以你永远不知道!
After reading the item, you should understand why it will help you use TypeScript more effectively. You’ll also know enough to understand if it doesn’t apply to your situation. Scott Meyers, the author of Effective C++, gives a memorable example of this. He met a team of engineers who wrote software that ran on missiles. They knew they could ignore his advice about preventing resource leaks, because their programs would always terminate when the missile hit the target and their hardware blew up. I’m not aware of any missiles with JavaScript runtimes, but the James Webb Space Telescope has one, so you never know!
最后,每一项都以“Things to Remember”结尾。这些是总结该项目的几个要点。如果您正在浏览,则可以阅读这些内容以了解该项目的内容以及您是否想阅读更多内容。您仍然应该阅读该项目!但是总结会在紧要关头起作用。
Finally, each item ends with “Things to Remember.” These are a few bullet points that summarize the item. If you’re skimming through, you can read these to get a sense for what the item is saying and whether you’d like to read more. You should still read the item! But the summary will do in a pinch.
全部代码示例是 TypeScript,除非从上下文中可以清楚地看出它们是 JSON、GraphQL 或其他某种语言。使用 TypeScript 的大部分经验都涉及与编辑器的交互,这在印刷中提出了一些挑战。我采用了一些约定来完成这项工作。
All code samples are TypeScript except where it’s clear from context that they are JSON, GraphQL, or some other language. Much of the experience of using TypeScript involves interacting with your editor, which presents some challenges in print. I’ve adopted a few conventions to make this work.
大多数编辑器使用波浪形下划线来显示错误。要查看完整的错误消息,请将鼠标悬停在带下划线的文本上。为了指示代码示例中的错误,我在错误发生位置下方的注释行中放置了波浪线:
Most editors surface errors using squiggly underlines. To see the full error message, you hover over the underlined text. To indicate an error in a code sample, I put squiggles in a comment line under the place where the error occurs:
letstr='not a number';letnum:number=str;// ~~~ Type 'string' is not assignable to type 'number'
letstr='not a number';letnum:number=str;// ~~~ Type 'string' is not assignable to type 'number'
为了清晰和简洁,我偶尔会编辑错误消息,但我从不删除错误。如果您将代码示例复制/粘贴到您的编辑器中,您应该得到准确指示的错误,不多也不少。
I occasionally edit the error messages for clarity and brevity, but I never remove an error. If you copy/paste a code sample into your editor, you should get exactly the errors indicated, no more no less.
为了提请注意缺少错误,我使用// OK:
To draw attention to the lack of an error, I use // OK:
letstr='not a number';letnum:number=strasany;// OK
letstr='not a number';letnum:number=strasany;// OK
您应该能够将鼠标悬停在编辑器中的一个符号上,以查看 TypeScript 认为它的类型是什么。为了在文本中表明这一点,我使用了以“type is”开头的注释:
You should be able to hover over a symbol in your editor to see what TypeScript considers its type. To indicate this in text, I use a comment starting with “type is”:
letv={str:'hello',num:42};// Type is { str: string; num: number; }
letv={str:'hello',num:42};// Type is { str: string; num: number; }
该类型用于行中的第一个符号(v在本例中)或函数调用的结果:
The type is for the first symbol on the line (v in this case) or for the result of a function call:
'four score'.split(' ');// Type is string[]
'four score'.split(' ');// Type is string[]
这与您在编辑器字符中看到的字符类型相匹配。在函数调用的情况下,您可能需要分配给临时变量以查看类型。
This matches the type you’d see in your editor character for character. In the case of function calls you may need to assign to a temporary variable to see the type.
我偶尔会引入 no-op 语句来指示特定代码行上的变量类型:
I will occasionally introduce no-op statements to indicate the type of a variable on a specific line of code:
functionfoo(x:string|string[]){if(Array.isArray(x)){x;// Type is string[]}else{x;// Type is string}}
functionfoo(x:string|string[]){if(Array.isArray(x)){x;// Type is string[]}else{x;// Type is string}}
这些x;行只是为了演示条件的每个分支中的类型。您不需要(也不应该)在自己的代码中包含这样的语句。
The x; lines are only there to demonstrate the type in each branch of the conditional. You don’t need to (and shouldn’t) include statements like this in your own code.
除非另有说明或上下文清楚,否则代码示例旨在使用标志进行检查--strict。所有示例都使用 TypeScript 3.7.0-beta 进行了验证。
Unless it’s otherwise noted or clear from context, code samples are intended to be checked with the --strict flag. All samples were verified using TypeScript 3.7.0-beta.
本书使用以下排版约定:
The following typographical conventions are used in this book:
表示新术语、URL、电子邮件地址、文件名和文件扩展名。
Indicates new terms, URLs, email addresses, filenames, and file extensions.
Constant widthConstant width用于程序列表,以及在段落中引用程序元素,例如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
Used for program listings, as well as within paragraphs to refer to program elements such as variable or function names, databases, data types, environment variables, statements, and keywords.
Constant width boldConstant width bold显示应由用户逐字输入的命令或其他文本。
Shows commands or other text that should be typed literally by the user.
Constant width italicConstant width italic显示应替换为用户提供的值或由上下文确定的值的文本。
Shows text that should be replaced with user-supplied values or by values determined by context.
此元素表示提示或建议。
This element signifies a tip or suggestion.
该元素表示一般注释。
This element signifies a general note.
该元素表示警告或警告。
This element indicates a warning or caution.
补充材料(代码示例、练习等)可从https://github.com/danvk/effective-typescript下载。
Supplemental material (code examples, exercises, etc.) is available for download at https://github.com/danvk/effective-typescript.
如果您在使用代码示例时遇到技术问题或问题,请发送电子邮件至bookquestions@oreilly.com。
If you have a technical question or a problem using the code examples, please send email to bookquestions@oreilly.com.
本书旨在帮助您完成工作。一般来说,如果本书提供了示例代码,您可以在您的程序和文档中使用它。除非您要复制代码的重要部分,否则无需联系我们获得许可。例如,编写一个使用本书中几段代码的程序不需要许可。销售或分发 O'Reilly 图书中的示例需要获得许可。通过引用本书和引用示例代码来回答问题不需要许可。将本书中的大量示例代码合并到您的产品文档中确实需要获得许可。
This book is here to help you get your job done. In general, if example code is offered with this book, you may use it in your programs and documentation. You do not need to contact us for permission unless you’re reproducing a significant portion of the code. For example, writing a program that uses several chunks of code from this book does not require permission. Selling or distributing examples from O’Reilly books does require permission. Answering a question by citing this book and quoting example code does not require permission. Incorporating a significant amount of example code from this book into your product’s documentation does require permission.
我们赞赏但通常不需要署名。署名通常包括书名、作者、出版商和 ISBN。例如:“ Effective TypeScript,作者 Dan Vanderkam (O'Reilly)。版权所有 2020 Dan Vanderkam,978-1-492-05374-3。”
We appreciate, but generally do not require, attribution. An attribution usually includes the title, author, publisher, and ISBN. For example: “Effective TypeScript by Dan Vanderkam (O’Reilly). Copyright 2020 Dan Vanderkam, 978-1-492-05374-3.”
如果您觉得您对代码示例的使用不属于合理使用或上述许可范围,请随时通过permissions@oreilly.com与我们联系。
If you feel your use of code examples falls outside fair use or the permission given above, feel free to contact us at permissions@oreilly.com.
40 多年来,O'Reilly Media提供技术和业务培训、知识和洞察力来帮助公司取得成功。
For more than 40 years, O’Reilly Media has provided technology and business training, knowledge, and insight to help companies succeed.
我们独特的专家和创新者网络通过书籍、文章、会议和我们的在线学习平台分享他们的知识和专长。O'Reilly 的在线学习平台让您可以按需访问实时培训课程、深度学习路径、交互式编码环境,以及来自 O'Reilly 和 200 多家其他出版商的大量文本和视频。欲了解更多信息,请访问http://oreilly.com。
Our unique network of experts and innovators share their knowledge and expertise through books, articles, conferences, and our online learning platform. O’Reilly’s online learning platform gives you on-demand access to live training courses, in-depth learning paths, interactive coding environments, and a vast collection of text and video from O’Reilly and 200+ other publishers. For more information, please visit http://oreilly.com.
请将有关本书的评论和问题发送给出版商:
Please address comments and questions concerning this book to the publisher:
您可以访问本书的网页,我们在其中列出了勘误表、示例和任何其他信息,网址为https://oreil.ly/Effective_TypeScript。
You can access the web page for this book, where we list errata, examples, and any additional information, at https://oreil.ly/Effective_TypeScript.
要对本书发表评论或提出技术问题,请发送电子邮件至bookquestions@oreilly.com。
To comment or ask technical questions about this book, send email to bookquestions@oreilly.com.
有关我们的书籍、课程、会议和新闻的更多信息,请访问我们的网站http://www.oreilly.com。
For more information about our books, courses, conferences, and news, see our website at http://www.oreilly.com.
在 Facebook 上找到我们: http: //facebook.com/oreilly
Find us on Facebook: http://facebook.com/oreilly
在 Twitter 上关注我们: http: //twitter.com/oreillymedia
Follow us on Twitter: http://twitter.com/oreillymedia
在 YouTube 上观看我们: http: //www.youtube.com/oreillymedia
Watch us on YouTube: http://www.youtube.com/oreillymedia
有很多人帮助使这本书成为可能。感谢 Evan Martin 向我介绍了 TypeScript 并向我展示了如何思考它。感谢 Douwe Osinga 将我与 O'Reilly 联系起来并支持该项目。感谢布雷特·斯拉特金 (Brett Slatkin) 在结构方面的建议,并向我展示了我认识的人可以写出一本Effective 的书。感谢 Scott Meyers 提出这种格式以及他的“Effective Effective Books”博客文章,其中提供了必要的指导。
There are many people who helped make this book possible. Thanks to Evan Martin for introducing me to TypeScript and showing me how to think about it. To Douwe Osinga for connecting me with O’Reilly and being supportive of the project. To Brett Slatkin for advice on structure and for showing me that someone I knew could write an Effective book. To Scott Meyers for coming up with this format and for his “Effective Effective Books” blog post, which provided essential guidance.
感谢我的审稿人 Rick Battagline、Ryan Cavanaugh、Boris Cherny、Yakov Fain、Jesse Hallett 和 Jason Killian。感谢我在 Sidewalk 多年来与我一起学习 TypeScript 的所有同事。感谢 O'Reilly 帮助本书诞生的每一个人:Angela Rufino、Jennifer Pollock、Deborah Baker、Nick Adams 和 Jasmine Kwityn。感谢 TypeScript NYC 工作人员 Jason、Orta 和 Kirill,以及所有演讲者。许多项目的灵感来自聚会上的演讲,如下表所述:
To my reviewers, Rick Battagline, Ryan Cavanaugh, Boris Cherny, Yakov Fain, Jesse Hallett, and Jason Killian. To all my coworkers at Sidewalk who learned TypeScript with me over the years. To everyone at O’Reilly who helped make this book happen: Angela Rufino, Jennifer Pollock, Deborah Baker, Nick Adams, and Jasmine Kwityn. To the TypeScript NYC crew, Jason, Orta, and Kirill, and to all the speakers. Many items were inspired by talks at the Meetup, as described in the following list:
第 3 条的灵感来自 Evan Martin 的一篇博客文章,在我第一次学习 TypeScript 时发现这篇文章特别有启发性。
Item 3 was inspired by a blog post of Evan Martin’s that I found particularly enlightening as I was first learning TypeScript.
第 7 项的灵感来自 Anders 在 2018 年 TSConf 上关于结构类型和keyof关系的演讲,以及 Jesse Hallett 在 2019 年 4 月 TypeScript NYC Meetup 上的演讲。
Item 7 was inspired by Anders’s talk about structural typing and keyof relationships at TSConf 2018, and by a talk of Jesse Hallett’s at the April 2019 TypeScript NYC Meetup.
Basarat 的指南以及 DeeV 和 GPicazo 在 Stack Overflow 上的有用回答对于编写第 9 项至关重要。
Both Basarat’s guide and helpful answers by DeeV and GPicazo on Stack Overflow were essential in writing Item 9.
第 10 条建立在Effective JavaScript (Addison-Wesley)的第 4 条中的类似建议之上。
Item 10 builds on similar advice in Item 4 of Effective JavaScript (Addison-Wesley).
在 2019 年 8 月的 TypeScript NYC Meetup 上,我受到围绕这个主题的大量混淆的启发,写了第 11 项。
I was inspired to write Item 11 by mass confusion around this topic at the August 2019 TypeScript NYC Meetup.
typeStack Overflow 上关于vs. 的几个问题极大地帮助了第 13 条interface。Jesse Hallett 提出了围绕可扩展性的表述。
Item 13 was greatly aided by several questions about type vs. interface on Stack Overflow. Jesse Hallett suggested the formulation around extensibility.
Jacob Baskin 对第 14 项提供了鼓励和早期反馈。
Jacob Baskin provided encouragement and early feedback on Item 14.
第 19 项的灵感来自提交给 r/typescript subreddit 的几个代码示例。
Item 19 was inspired by several code samples submitted to the r/typescript subreddit.
第 26 项基于我自己在 Medium 上的文章和我在 2018 年 10 月 TypeScript NYC Meetup 上的演讲。
Item 26 is based on my own writing on Medium and a talk I gave at the October 2018 TypeScript NYC Meetup.
第 28 条基于 Haskell 中的常见建议(“使非法状态无法表示”)。法航 447 的故事灵感来自 Jeff Wise 2011 年在《大众机械》上发表的精彩文章。
Item 28 is based on common advice in Haskell (“make illegal states unrepresentable”). The Air France 447 story is inspired by Jeff Wise’s incredible 2011 article in Popular Mechanics.
第 29 条基于我在 Mapbox 类型声明中遇到的一个问题。Jason Killian 建议标题中的措辞。
Item 29 is based on an issue I ran into with the Mapbox type declarations. Jason Killian suggested the phrasing in the title.
Item 36中关于命名的建议很常见,但这个特定的表述是受 Dan North 在97 Things Every Programmer Should Know (O'Reilly)中的短文的启发。
The advice about naming in Item 36 is common but this particular formulation was inspired by Dan North’s short article in 97 Things Every Programmer Should Know (O’Reilly).
第 37 项的灵感来自 Jason Killian 在 2017 年 9 月的第一次 TypeScript NYC Meetup 上的演讲。
Item 37 was inspired by Jason Killian’s talk at the very first TypeScript NYC Meetup in September 2017.
第 41 项基于 TypeScript 2.1 发行说明。“进化任何”一词在 TypeScript 编译器本身之外并未广泛使用,但我发现为这种不寻常的模式命名很有用。
Item 41 is based on the TypeScript 2.1 release notes. The term “evolving any” is not widely used outside the TypeScript compiler itself, but I find it useful to have a name for this unusual pattern.
第 42 项的灵感来自 Jesse Hallett 的一篇博文。Titian Cernicova Dragomir 在 TypeScript 问题 #33128 中的反馈极大地帮助了项目 43 。
Item 42 was inspired by a blog post of Jesse Hallett’s. Item 43 was greatly aided by feedback from Titian Cernicova Dragomir in TypeScript issue #33128.
第 44 项基于 York Yao 在该type-coverage工具上的工作。我想要这样的东西,它存在!
Item 44 is based on York Yao’s work on the type-coverage tool. I wanted something like this and it existed!
第 46 项基于我在 2017 年 12 月 TypeScript NYC Meetup 上的演讲。
Item 46 is based on a talk I gave at the December 2017 TypeScript NYC Meetup.
第 50 条要感谢 David Sheldrick 在Artsy博客上关于条件类型的帖子,这对我来说极大地揭开了这个话题的神秘面纱。
Item 50 owes a debt of gratitude to David Sheldrick’s post on the Artsy blog on conditional types, which greatly demystified the topic for me.
第 51 项的灵感来自 Steve Faulkner aka southpolesteve 在 2019 年 2 月聚会上的演讲。
Item 51 was inspired by a talk Steve Faulkner aka southpolesteve gave at the February 2019 Meetup.
第 52 条基于我自己在 Medium 上的写作和 typings-checker 工具的工作,它最终被合并到 dtslint 中。
Item 52 is based on my own writing on Medium and work on the typings-checker tool, which eventually got folded into dtslint.
第 53 条的灵感来自于 Kat Busch 在 Medium 上关于 TypeScript 中各种枚举类型的帖子,以及 Boris Cherny 在Programming TypeScript (O'Reilly) 中关于该主题的文章。
Item 53 was inspired/reinforced by Kat Busch’s Medium post on the various types of enums in TypeScript, as well as Boris Cherny’s writings on this topic in Programming TypeScript (O’Reilly).
第 54 条的灵感来自于我自己和我的同事在这个话题上的困惑。Anders 在 TypeScript PR #12253 上给出了明确的解释。
Item 54 was inspired by my own confusion and that of my coworkers on this topic. The definitive explanation is given by Anders on TypeScript PR #12253.
MDN 文档对于编写Item 55至关重要。
The MDN documentation was essential for writing Item 55.
第 56 条大致基于Effective JavaScript (Addison-Wesley) 的第 35 条。
Item 56 is loosely based on Item 35 of Effective JavaScript (Addison-Wesley).
第 8 章基于我自己迁移老化的 dygraphs 库的经验。
Chapter 8 is based on my own experience migrating the aging dygraphs library.
我通过优秀的 r/typescript subreddit 找到了许多导致本书诞生的博客文章和演讲。我特别感谢提供代码示例的开发人员,这些代码示例对于理解初学者 TypeScript 中的常见问题至关重要。感谢 Marius Schulz 的 TypeScript 周报。虽然它只是偶尔每周一次,但它始终是极好的材料来源,也是跟上 TypeScript 的好方法。感谢 Anders、Daniel、Ryan 和 Microsoft 的整个 TypeScript 团队,感谢他们的会谈和对问题的所有反馈。我的大部分问题都是误解,但没有什么比提交错误并立即看到 Anders Hejlsberg 亲自修复它更令人满意的了!最后,感谢亚历克斯在这个项目中给予的支持,以及对所有工作假期、早上、晚上的理解,
I found many of the blog posts and talks that led to this book through the excellent r/typescript subreddit. I’m particularly grateful to developers who provided code samples there which were essential for understanding common issues in beginner TypeScript. Thanks to Marius Schulz for the TypeScript Weekly newsletter. While it’s only occasionally weekly, it’s always an excellent source of material and a great way to keep up with TypeScript. To Anders, Daniel, Ryan, and the whole TypeScript team at Microsoft for the talks and all the feedback on issues. Most of my issues were misunderstandings, but there is nothing quite so satisfying as filing a bug and immediately seeing Anders Hejlsberg himself fix it! Finally, thanks to Alex for being so supportive during this project and so understanding of all the working vacations, mornings, evenings, and weekends I needed to complete it.
在我们深入细节之前,本章将帮助您了解 TypeScript 的全局。它是什么以及您应该如何看待它?它与 JavaScript 有什么关系?它的类型是否可以为空?这是怎么回事any?还有鸭子?
Before we dive into the details, this chapter helps you understand the big picture of TypeScript. What is it and how should you think about it? How does it relate to JavaScript? Are its types nullable or are they not? What’s this about any? And ducks?
TypeScript 作为一种语言有点不寻常,因为它既不在解释器中运行(像 Python 和 Ruby 那样)也不编译成较低级别的语言(如 Java 和 C 所做的)。相反,它会编译成另一种高级语言 JavaScript。运行的是这个 JavaScript,而不是你的 TypeScript。所以 TypeScript 与 JavaScript 的关系是必不可少的,但它也可能是混淆的根源。了解这种关系将帮助您成为更高效的 TypeScript 开发人员。
TypeScript is a bit unusual as a language in that it neither runs in an interpreter (as Python and Ruby do) nor compiles down to a lower-level language (as Java and C do). Instead, it compiles to another high-level language, JavaScript. It is this JavaScript that runs, not your TypeScript. So TypeScript’s relationship with JavaScript is essential, but it can also be a source of confusion. Understanding this relationship will help you be a more effective TypeScript developer.
TypeScript 的类型系统还有一些您应该注意的不寻常方面。后面的章节将更详细地介绍类型系统,但这一章将提醒您注意它所包含的一些惊喜。
TypeScript’s type system also has some unusual aspects that you should be aware of. Later chapters cover the type system in much greater detail, but this one will alert you to some of the surprises that it has in store.
如果如果你长期使用 TypeScript,你将不可避免地听到“TypeScript is a superset of JavaScript”或“TypeScript is a typed superset of JavaScript”这样的说法。但这到底是什么意思?TypeScript 和 JavaScript 之间的关系是什么?由于这些语言之间的联系如此紧密,因此深入了解它们之间的关系是很好地使用 TypeScript 的基础。
If you use TypeScript for long, you’ll inevitably hear the phrase “TypeScript is a superset of JavaScript” or “TypeScript is a typed superset of JavaScript.” But what does this mean, exactly? And what is the relationship between TypeScript and JavaScript? Since these languages are so closely linked, a strong understanding of how they relate to each is the foundation for using TypeScript well.
TypeScript 在语法意义上是 JavaScript 的超集:只要你的 JavaScript 程序没有任何语法错误,那么它也是一个 TypeScript 程序。TypeScript 的类型检查器很可能会标记您的代码的一些问题。但这是一个独立的问题。TypeScript 仍会解析您的代码并发出 JavaScript。(这是关系的另一个关键部分。我们将在第 3 项中对此进行更多探讨。)
TypeScript is a superset of JavaScript in a syntactic sense: so long as your JavaScript program doesn’t have any syntax errors then it is also a TypeScript program. It’s quite likely that TypeScript’s type checker will flag some issues with your code. But this is an independent problem. TypeScript will still parse your code and emit JavaScript. (This is another key part of the relationship. We’ll explore this more in Item 3.)
TypeScript 文件使用.ts(或.tsx)扩展名,而不是JavaScript 文件的.js(或.jsx )扩展名。这并不意味着 TypeScript 是一种完全不同的语言!由于 TypeScript 是 JavaScript 的超集,因此.js文件中的代码已经是 TypeScript。将main.js重命名为main.ts不会改变这一点。
TypeScript files use a .ts (or .tsx) extension, rather than the .js (or .jsx) extension of a JavaScript file. This doesn’t mean that TypeScript is a completely different language! Since TypeScript is a superset of JavaScript, the code in your .js files is already TypeScript. Renaming main.js to main.ts doesn’t change that.
这如果您要将现有的 JavaScript 代码库迁移到 TypeScript,这将非常有用。这意味着您无需用另一种语言重写任何代码即可开始使用 TypeScript 并获得它提供的好处。这如果您选择用 Java 等语言重写 JavaScript,则情况并非如此。这种温和的迁移路径是 TypeScript 的最佳特性之一。第 8 章将对这个主题进行更多讨论。
This is enormously helpful if you’re migrating an existing JavaScript codebase to TypeScript. It means that you don’t have to rewrite any of your code in another language to start using TypeScript and get the benefits it provides. This would not be true if you chose to rewrite your JavaScript in a language like Java. This gentle migration path is one of the best features of TypeScript. There will be much more to say about this topic in Chapter 8.
所有 JavaScript 程序都是 TypeScript 程序,但反之则不然:存在不是 JavaScript 程序的 TypeScript 程序。这是因为 TypeScript 添加了额外的语法来指定类型。(它添加了一些其他语法,主要是出于历史原因。请参阅条目 53。)
All JavaScript programs are TypeScript programs, but the converse is not true: there are TypeScript programs which are not JavaScript programs. This is because TypeScript adds additional syntax for specifying types. (There are some other bits of syntax it adds, largely for historical reasons. See Item 53.)
例如,这是一个有效的 TypeScript 程序:
For instance, this is a valid TypeScript program:
functiongreet(who:string){console.log('Hello',who);}
functiongreet(who:string){console.log('Hello',who);}
但是当你通过一个node需要 JavaScript 的程序运行它时,你会得到一个错误:
But when you run this through a program like node that expects JavaScript, you’ll get an error:
函数问候(谁:字符串){
^
语法错误:意外的标记:function greet(who: string) {
^
SyntaxError: Unexpected token :
是: string特定于 TypeScript 的类型注释。一旦你使用了它,你就超越了普通的 JavaScript(见图1-1)。
The : string is a type annotation that is specific to TypeScript. Once you use one, you’ve gone beyond plain JavaScript (see Figure 1-1).
这并不是说 TypeScript 不为纯 JavaScript 程序提供价值。确实如此!例如,这个 JavaScript 程序:
This is not to say that TypeScript doesn’t provide value for plain JavaScript programs. It does! For example, this JavaScript program:
letcity='new york city';console.log(city.toUppercase());
letcity='new york city';console.log(city.toUppercase());
运行时会报错:
will throw an error when you run it:
类型错误:city.toUppercase 不是函数
TypeError: city.toUppercase is not a function
该程序中没有类型注释,但 TypeScript 的类型检查器仍然能够发现问题:
There are no type annotations in this program, but TypeScript’s type checker is still able to spot the problem:
letcity='new york city';console.log(city.toUppercase());// ~~~~~~~~~~~ Property 'toUppercase' does not exist on type// 'string'. Did you mean 'toUpperCase'?
letcity='new york city';console.log(city.toUppercase());// ~~~~~~~~~~~ Property 'toUppercase' does not exist on type// 'string'. Did you mean 'toUpperCase'?
您不必告诉 TypeScript 的类型是city:string它是从初始值推断出来的。类型推断是 TypeScript 的关键部分,第 3 章探讨了如何很好地使用它。
You didn’t have to tell TypeScript that the type of city was string: it inferred it from the initial value. Type inference is a key part of TypeScript and Chapter 3 explores how to use it well.
TypeScript 类型系统的目标之一是检测将在运行时抛出异常的代码,而无需运行您的代码。当您听到 TypeScript 被描述为“静态”类型系统时,它指的就是这个。类型检查器不能总是发现会抛出异常的代码,但它会尝试。
One of the goals of TypeScript’s type system is to detect code that will throw an exception at runtime, without having to run your code. When you hear TypeScript described as a “static” type system, this is what it refers to. The type checker cannot always spot code that will throw exceptions, but it will try.
即使您的代码没有抛出异常,它仍然可能不会按照您的意图进行。TypeScript 也试图捕捉其中的一些问题。例如,这个 JavaScript程序:
Even if your code doesn’t throw an exception, it still might not do what you intend. TypeScript tries to catch some of these issues, too. For example, this JavaScript program:
conststates=[{name:'Alabama',capital:'Montgomery'},{name:'Alaska',capital:'Juneau'},{name:'Arizona',capital:'Phoenix'},// ...];for(conststateofstates){console.log(state.capitol);}
conststates=[{name:'Alabama',capital:'Montgomery'},{name:'Alaska',capital:'Juneau'},{name:'Arizona',capital:'Phoenix'},// ...];for(conststateofstates){console.log(state.capitol);}
将记录:
will log:
不明确的 不明确的 不明确的
undefined undefined undefined
哎呀!什么地方出了错?该程序是有效的 JavaScript(因此也是 TypeScript)。它运行时没有抛出任何错误。但它显然没有按照你的意图去做。即使不添加类型注释,TypeScript 的类型检查器也能够发现错误(并提供有用的建议):
Whoops! What went wrong? This program is valid JavaScript (and hence TypeScript). And it ran without throwing any errors. But it clearly didn’t do what you intended. Even without adding type annotations, TypeScript’s type checker is able to spot the error (and offer a helpful suggestion):
for(conststateofstates){console.log(state.capitol);// ~~~~~~~ Property 'capitol' does not exist on type// '{ name: string; capital: string; }'.// Did you mean 'capital'?}
for(conststateofstates){console.log(state.capitol);// ~~~~~~~ Property 'capitol' does not exist on type// '{ name: string; capital: string; }'.// Did you mean 'capital'?}
虽然即使您不提供类型注释,TypeScript 也可以捕获错误,但如果您提供类型注释,它就能完成更彻底的工作。这是因为类型注释告诉 TypeScript 你的意图是什么,这让它可以发现代码行为与你的意图不匹配的地方。例如,如果您在前面的示例中扭转了capital/拼写错误怎么办?capitol
While TypeScript can catch errors even if you don’t provide type annotations, it’s able to do a much more thorough job if you do. This is because type annotations tell TypeScript what your intent is, and this lets it spot places where your code’s behavior does not match your intent. For example, what if you’d reversed the capital/capitol spelling mistake in the previous example?
conststates=[{name:'Alabama',capitol:'Montgomery'},{name:'Alaska',capitol:'Juneau'},{name:'Arizona',capitol:'Phoenix'},// ...];for(conststateofstates){console.log(state.capital);// ~~~~~~~ Property 'capital' does not exist on type// '{ name: string; capitol: string; }'.// Did you mean 'capitol'?}
conststates=[{name:'Alabama',capitol:'Montgomery'},{name:'Alaska',capitol:'Juneau'},{name:'Arizona',capitol:'Phoenix'},// ...];for(conststateofstates){console.log(state.capital);// ~~~~~~~ Property 'capital' does not exist on type// '{ name: string; capitol: string; }'.// Did you mean 'capitol'?}
之前非常有用的错误现在完全错了!问题是你用两种不同的方式拼写了同一个属性,而 TypeScript 不知道哪一个是正确的。它可以猜测,但不一定总是正确的。解决方案是通过明确声明以下类型来阐明您的意图states:
The error that was so helpful before now gets it exactly wrong! The problem is that you’ve spelled the same property two different ways, and TypeScript doesn’t know which one is right. It can guess, but it may not always be correct. The solution is to clarify your intent by explicitly declaring the type of states:
interfaceState{name:string;capital:string;}conststates:State[]=[{name:'Alabama',capitol:'Montgomery'},// ~~~~~~~~~~~~~~~~~~~~~{name:'Alaska',capitol:'Juneau'},// ~~~~~~~~~~~~~~~~~{name:'Arizona',capitol:'Phoenix'},// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known// properties, but 'capitol' does not exist in type// 'State'. Did you mean to write 'capital'?// ...];for(conststateofstates){console.log(state.capital);}
interfaceState{name:string;capital:string;}conststates:State[]=[{name:'Alabama',capitol:'Montgomery'},// ~~~~~~~~~~~~~~~~~~~~~{name:'Alaska',capitol:'Juneau'},// ~~~~~~~~~~~~~~~~~{name:'Arizona',capitol:'Phoenix'},// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known// properties, but 'capitol' does not exist in type// 'State'. Did you mean to write 'capital'?// ...];for(conststateofstates){console.log(state.capital);}
现在错误与问题匹配,建议的修复是正确的。通过阐明我们的意图,您还帮助 TypeScript 发现了其他潜在问题。例如,如果您capitol在数组中只拼错一次,那么之前就不会出现错误。但是对于类型注解,有:
Now the errors match the problem and the suggested fix is correct. By spelling out our intent, you’ve also helped TypeScript spot other potential problems. For instance, had you only misspelled capitol once in the array, there wouldn’t have been an error before. But with the type annotation, there is:
conststates:State[]=[{name:'Alabama',capital:'Montgomery'},{name:'Alaska',capitol:'Juneau'},// ~~~~~~~~~~~~~~~~~ Did you mean to write 'capital'?{name:'Arizona',capital:'Phoenix'},// ...];
conststates:State[]=[{name:'Alabama',capital:'Montgomery'},{name:'Alaska',capitol:'Juneau'},// ~~~~~~~~~~~~~~~~~ Did you mean to write 'capital'?{name:'Arizona',capital:'Phoenix'},// ...];
根据维恩图,我们可以添加一组新的程序:通过类型检查器的 TypeScript 程序(见图1-2)。
In terms of the Venn diagram, we can add in a new group of programs: TypeScript programs which pass the type checker (see Figure 1-2).
如果您觉得“TypeScript 是 JavaScript 的超集”这一说法有误,那可能是因为您正在考虑图中的第三组程序。实际上,这是与使用 TypeScript 的日常体验最相关的一个。通常,当您使用 TypeScript 时,您会尝试让您的代码通过所有类型检查。
If the statement that “TypeScript is a superset of JavaScript” feels wrong to you, it may be because you’re thinking of this third set of programs in the diagram. In practice, this is the most relevant one to the day-to-day experience of using TypeScript. Generally when you use TypeScript, you try to keep your code passing all the type checks.
TypeScript 的类型系统模拟了JavaScript 的运行时行为。如果您来自具有更严格的运行时检查的语言,这可能会导致一些意外。例如:
TypeScript’s type system models the runtime behavior of JavaScript. This may result in some surprises if you’re coming from a language with stricter runtime checks. For example:
constx=2+'3';// OK, type is stringconsty='2'+3;// OK, type is string
constx=2+'3';// OK, type is stringconsty='2'+3;// OK, type is string
这些语句都通过了类型检查器,即使它们是有问题的并且在许多其他语言中确实会产生运行时错误。但这确实模拟了 JavaScript 的运行时行为,其中两个表达式都会产生 string "23"。
These statements both pass the type checker, even though they are questionable and do produce runtime errors in many other languages. But this does model the runtime behavior of JavaScript, where both expressions result in the string "23".
不过,TypeScript 确实在某处画了线。类型检查器会标记所有这些语句中的问题,即使它们在运行时不会抛出异常:
TypeScript does draw the line somewhere, though. The type checker flags issues in all of these statements, even though they do not throw exceptions at runtime:
consta=null+7;// Evaluates to 7 in JS// ~~~~ Operator '+' cannot be applied to types ...constb=[]+12;// Evaluates to '12' in JS// ~~~~~~~ Operator '+' cannot be applied to types ...alert('Hello','TypeScript');// alerts "Hello"// ~~~~~~~~~~~~ Expected 0-1 arguments, but got 2
consta=null+7;// Evaluates to 7 in JS// ~~~~ Operator '+' cannot be applied to types ...constb=[]+12;// Evaluates to '12' in JS// ~~~~~~~ Operator '+' cannot be applied to types ...alert('Hello','TypeScript');// alerts "Hello"// ~~~~~~~~~~~~ Expected 0-1 arguments, but got 2
TypeScript 类型系统的指导原则是它应该模拟 JavaScript 的运行时行为。但在所有这些情况下,TypeScript 认为奇怪的用法更有可能是错误的结果,而不是开发人员的意图,因此它不仅仅是对运行时行为进行建模。capital我们在/ example中看到了另一个例子capitol,其中程序没有抛出(它记录了undefined)但类型检查器仍然标记了一个错误。
The guiding principle of TypeScript’s type system is that it should model JavaScript’s runtime behavior. But in all of these cases, TypeScript considers it more likely that the odd usage is the result of an error than the developer’s intent, so it goes beyond simply modeling the runtime behavior. We saw another example of this in the capital/capitol example, where the program didn’t throw (it logged undefined) but the type checker still flagged an error.
TypeScript 如何决定何时模拟 JavaScript 的运行时行为以及何时超越它?归根结底,这是一个品味问题。通过采用 TypeScript,你相信构建它的团队的判断。如果您喜欢添加nulland7或[]and 12,或者使用多余的参数调用函数,那么 TypeScript 可能不适合您!
How does TypeScript decide when to model JavaScript’s runtime behavior and when to go beyond it? Ultimately this is a matter of taste. By adopting TypeScript you’re trusting the judgment of the team that builds it. If you enjoy adding null and 7 or [] and 12, or calling functions with superfluous arguments, then TypeScript might not be for you!
如果您的程序进行了类型检查,它还会在运行时抛出错误吗?答案是“是的”。这是一个例子:
If your program type checks, could it still throw an error at runtime? The answer is “yes.” Here’s an example:
constnames=['Alice','Bob'];console.log(names[2].toUpperCase());
constnames=['Alice','Bob'];console.log(names[2].toUpperCase());
当你运行它时,它会抛出:
When you run this, it throws:
类型错误:无法读取未定义的属性“toUpperCase”
TypeError: Cannot read property 'toUpperCase' of undefined
TypeScript 假定数组访问在范围内,但事实并非如此。结果是一个例外。
TypeScript assumed the array access would be within bounds, but it was not. The result was an exception.
当您使用该类型时,也经常会出现未捕获的错误any,我们将在第 5 项中讨论,并在第 5 章中更详细地讨论。
Uncaught errors also frequently come up when you use the any type, which we’ll discuss in Item 5 and in more detail in Chapter 5.
这些异常的根本原因是 TypeScript 对值的类型和现实的理解发生了分歧。可以保证其静态类型的准确性的类型系统被认为是可靠的。TypeScript 的类型系统非常不健全,也从未打算如此。如果健全性对你很重要,你可能想看看其他语言,比如 Reason 或 Elm。虽然这些确实提供了更多的运行时安全保证,但这是有代价的:两者都不是 JavaScript 的超集,因此迁移会更加复杂。
The root cause of these exceptions is that TypeScript’s understanding of a value’s type and reality have diverged. A type system which can guarantee the accuracy of its static types is said to be sound. TypeScript’s type system is very much not sound, nor was it ever intended to be. If soundness is important to you, you may want to look at other languages like Reason or Elm. While these do offer more guarantees of runtime safety, this comes at a cost: neither is a superset of JavaScript, so migration will be more complicated.
TypeScript 是 JavaScript 的超集。也就是说,所有的 JavaScript 程序都已经是 TypeScript 程序了。TypeScript 有一些自己的语法,所以 TypeScript 程序通常不是有效的 JavaScript 程序。
TypeScript is a superset of JavaScript. In other words, all JavaScript programs are already TypeScript programs. TypeScript has some syntax of its own, so TypeScript programs are not, in general, valid JavaScript programs.
TypeScript 添加了一个类型系统来模拟 JavaScript 的运行时行为,并尝试发现将在运行时抛出异常的代码。但是您不应该期望它会标记每个异常。代码有可能通过了类型检查器,但仍然在运行时抛出异常。
TypeScript adds a type system that models JavaScript’s runtime behavior and tries to spot code which will throw exceptions at runtime. But you shouldn’t expect it to flag every exception. It is possible for code to pass the type checker but still throw at runtime.
虽然 TypeScript 的类型系统在很大程度上模拟了 JavaScript 的行为,但也有一些 JavaScript 允许但 TypeScript 选择禁止的结构,例如使用错误数量的参数调用函数。这在很大程度上是一个品味问题。
While TypeScript’s type system largely models JavaScript behavior, there are some constructs that JavaScript allows but TypeScript chooses to bar, such as calling functions with the wrong number of arguments. This is largely a matter of taste.
Does this code pass the type checker?
functionadd(a,b){returna+b;}add(10,null);
functionadd(a,b){returna+b;}add(10,null);
在不知道您使用的是哪个选项的情况下,这是不可能的!TypeScript 编译器有大量这样的集合,在撰写本文时将近 100 个。
Without knowing which options you’re using, it’s impossible to say! The TypeScript compiler has an enormous set of these, nearly 100 at the time of this writing.
它们可以通过命令行设置:
They can be set via the command line:
$ tsc --noImplicitAny 程序.ts
$ tsc --noImplicitAny program.ts
或者通过配置文件tsconfig.json:
or via a configuration file, tsconfig.json:
{"compilerOptions":{"noImplicitAny":true}}
{"compilerOptions":{"noImplicitAny":true}}
您应该更喜欢配置文件。它确保你的同事和工具都知道你打算如何使用 TypeScript。您可以通过运行创建一个tsc --init。
You should prefer the configuration file. It ensures that your coworkers and tools all know exactly how you plan to use TypeScript. You can create one by running tsc --init.
许多 TypeScript 的配置设置控制它在何处查找源文件以及它生成的输出类型。但是一些控制语言本身的核心方面。这些是大多数语言不会留给用户的高级设计选择。根据配置方式的不同,TypeScript 感觉就像是一种非常不同的语言。要有效地使用它,您应该了解这些设置中最重要的部分:noImplicitAny和strictNullChecks。
Many of TypeScript’s configuration settings control where it looks for source files and what sort of output it generates. But a few control core aspects of the language itself. These are high-level design choices that most languages do not leave to their users. TypeScript can feel like a very different language depending on how it is configured. To use it effectively, you should understand the most important of these settings: noImplicitAny and strictNullChecks.
noImplicitAny控制变量是否必须具有已知类型。noImplicitAny此代码在关闭时有效:
noImplicitAny controls whether variables must have known types. This code is valid when noImplicitAny is off:
functionadd(a,b){returna+b;}
functionadd(a,b){returna+b;}
如果您将鼠标悬停add在编辑器中的符号上,它将显示 TypeScript 对该函数类型的推断:
If you mouse over the add symbol in your editor, it will reveal what TypeScript has inferred about the type of that function:
函数添加(a:任何,b:任何):任何
function add(a: any, b: any): any
这些any类型有效地禁用了涉及这些参数的代码的类型检查器。any是一个有用的工具,但应谨慎使用。有关更多信息any,请参阅第 5 项和第 3 章。
The any types effectively disable the type checker for code involving these parameters. any is a useful tool, but it should be used with caution. For much more on any, see Item 5 and Chapter 3.
这些被称为implicitany是因为你从来没有写过“any”这个词,但仍然会遇到危险的any类型。如果您设置选项,这将成为一个错误noImplicitAny:
These are called implicit anys because you never wrote the word “any” but still wound up with dangerous any types. This becomes an error if you set the noImplicitAny option:
functionadd(a,b){// ~ Parameter 'a' implicitly has an 'any' type// ~ Parameter 'b' implicitly has an 'any' typereturna+b;}
functionadd(a,b){// ~ Parameter 'a' implicitly has an 'any' type// ~ Parameter 'b' implicitly has an 'any' typereturna+b;}
这些错误可以通过显式编写类型声明来修复,或者: any是更具体的类型:
These errors can be fixed by explicitly writing type declarations, either : any or a more specific type:
functionadd(a:number,b:number){returna+b;}
functionadd(a:number,b:number){returna+b;}
noImplicitAny当 TypeScript 有类型信息时,它是最有用的,所以你应该确保尽可能设置。一旦您习惯了所有具有类型的变量,没有类型的 TypeScriptnoImplicitAny感觉就像是一种不同的语言。
TypeScript is the most helpful when it has type information, so you should be sure to set noImplicitAny whenever possible. Once you grow accustomed to all variables having types, TypeScript without noImplicitAny feels almost like a different language.
对于新项目,您应该从 on 开始noImplicitAny,以便在编写代码时编写类型。这将有助于 TypeScript 发现问题,提高代码的可读性,并增强您的开发体验(参见条款 6)。noImplicitAny仅当您将项目从 JavaScript 转换为 TypeScript 时才适合离开(请参阅第8 章)。
For new projects, you should start with noImplicitAny on, so that you write the types as you write your code. This will help TypeScript spot problems, improve the readability of your code, and enhance your development experience (see Item 6). Leaving noImplicitAny off is only appropriate if you’re transitioning a project from JavaScript to TypeScript (see Chapter 8).
strictNullChecks控制 和 是否null是undefined每种类型中的允许值。
strictNullChecks controls whether null and undefined are permissible values in every type.
strictNullChecks此代码在关闭时有效:
This code is valid when strictNullChecks is off:
constx:number=null;// OK, null is a valid number
constx:number=null;// OK, null is a valid number
但是当你打开时会触发错误strictNullChecks:
but triggers an error when you turn strictNullChecks on:
constx:number=null;// ~ Type 'null' is not assignable to type 'number'
constx:number=null;// ~ Type 'null' is not assignable to type 'number'
undefined如果您使用而不是,也会发生类似的错误null。
A similar error would have occurred had you used undefined instead of null.
如果你的意思是 allow null,你可以通过明确你的意图来修复错误:
If you mean to allow null, you can fix the error by making your intent explicit:
constx:number|null=null;
constx:number|null=null;
如果您不想允许null,则需要追踪它的来源并添加检查或断言:
If you do not wish to permit null, you’ll need to track down where it came from and add either a check or an assertion:
constel=document.getElementById('status');el.textContent='Ready';// ~~ Object is possibly 'null'if(el){el.textContent='Ready';// OK, null has been excluded}el!.textContent='Ready';// OK, we've asserted that el is non-null
constel=document.getElementById('status');el.textContent='Ready';// ~~ Object is possibly 'null'if(el){el.textContent='Ready';// OK, null has been excluded}el!.textContent='Ready';// OK, we've asserted that el is non-null
strictNullChecksnull对于捕获涉及和值的错误非常有帮助undefined,但它确实增加了使用该语言的难度。如果您要开始一个新项目,请尝试设置strictNullChecks. 但是,如果您是该语言的新手或正在迁移 JavaScript 代码库,您可能会选择将其关闭。你当然应该noImplicitAny先设置再设置strictNullChecks。
strictNullChecks is tremendously helpful for catching errors involving null and undefined values, but it does increase the difficulty of using the language. If you’re starting a new project, try setting strictNullChecks. But if you’re new to the language or migrating a JavaScript codebase, you may elect to leave it off. You should certainly set noImplicitAny before you set strictNullChecks.
如果您选择不使用strictNullChecks,请留意可怕的“undefined is not an object”运行时错误。其中每一项都提醒您应该考虑启用更严格的检查。随着项目的增长,更改此设置只会变得更加困难,因此在启用它之前尽量不要等待太久。
If you choose to work without strictNullChecks, keep an eye out for the dreaded “undefined is not an object” runtime error. Every one of these is a reminder that you should consider enabling stricter checking. Changing this setting will only get harder as your project grows, so try not to wait too long before enabling it.
那里还有许多影响语言语义的其他设置(例如,noImplicitThis和strictFunctionTypes),但与noImplicitAny和相比,这些都是次要的strictNullChecks。要启用所有这些检查,请打开strict设置。TypeScript 能够捕获最多的错误strict,所以这就是你最终想要结束的地方。
There are many other settings that affect language semantics (e.g., noImplicitThis and strictFunctionTypes), but these are minor compared to noImplicitAny and strictNullChecks. To enable all of these checks, turn on the strict setting. TypeScript is able to catch the most errors with strict, so this is where you eventually want to wind up.
知道您正在使用哪些选项!如果同事分享了一个 TypeScript 示例,而您无法重现他们的错误,请确保您的编译器选项相同。
Know which options you’re using! If a coworker shares a TypeScript example and you’re unable to reproduce their errors, make sure your compiler options are the same.
TypeScript 编译器包括几个影响语言核心方面的设置。
The TypeScript compiler includes several settings which affect core aspects of the language.
使用tsconfig.json而不是命令行选项配置 TypeScript 。
Configure TypeScript using tsconfig.json rather than command-line options.
noImplicitAny除非您要将 JavaScript 项目转换为TypeScript,否则请打开。
Turn on noImplicitAny unless you are transitioning a JavaScript project to TypeScript.
用于strictNullChecks防止“undefined is not an object”类型的运行时错误。
Use strictNullChecks to prevent “undefined is not an object”-style runtime errors.
Aim to enable strict to get the most thorough checking that TypeScript can offer.
At a high level, tsc (the TypeScript compiler) does two things:
它将下一代 TypeScript/JavaScript 转换为可在浏览器中运行的旧版 JavaScript(“转译”)。
It converts next-generation TypeScript/JavaScript to an older version of JavaScript that works in browsers (“transpiling”).
它会检查您的代码是否存在类型错误。
It checks your code for type errors.
令人惊讶的是,这两种行为完全相互独立。换句话说,代码中的类型不会影响 TypeScript 发出的 JavaScript。由于执行的是这个 JavaScript,这意味着您的类型不会影响代码的运行方式。
What’s surprising is that these two behaviors are entirely independent of one another. Put another way, the types in your code cannot affect the JavaScript that TypeScript emits. Since it’s this JavaScript that gets executed, this means that your types can’t affect the way your code runs.
这有一些令人惊讶的含义,并且应该告诉您关于 TypeScript 能为您做什么和不能为您做什么的期望。
This has some surprising implications and should inform your expectations about what TypeScript can and cannot do for you.
因为代码输出独立于类型检查,因此有类型错误的代码可以产生输出!
Because code output is independent of type checking, it follows that code with type errors can produce output!
$猫测试.ts 让 x = '你好'; x = 1234; $ tsc 测试.ts test.ts:2:1 - 错误 TS2322:类型“1234”不可分配给类型“string” 2 x = 1234; ~ $猫测试.js var x = '你好'; x = 1234;
$ cat test.ts let x = 'hello'; x = 1234; $ tsc test.ts test.ts:2:1 - error TS2322: Type '1234' is not assignable to type 'string' 2 x = 1234; ~ $ cat test.js var x = 'hello'; x = 1234;
如果您来自 C 或类型检查和输出齐头并进的 Java。您可以将所有 TypeScript 错误视为类似于这些语言中的警告:它们很可能表明存在问题并且值得调查,但它们不会停止构建。
This can be quite surprising if you’re coming from a language like C or Java where type checking and output go hand in hand. You can think of all TypeScript errors as being similar to warnings in those languages: it’s likely that they indicate a problem and are worth investigating, but they won’t stop the build.
在存在错误的情况下发出代码在实践中很有帮助。如果您正在构建 Web 应用程序,您可能知道它的特定部分存在问题。但由于 TypeScript 仍会在出现错误时生成代码,因此您可以在修复它们之前测试应用程序的其他部分。
Code emission in the presence of errors is helpful in practice. If you’re building a web application, you may know that there are problems with a particular part of it. But because TypeScript will still generate code in the presence of errors, you can test the other parts of your application before you fix them.
提交代码时应以零错误为目标,以免陷入必须记住什么是预期错误或意外错误的陷阱。如果您想禁用错误输出,您可以使用tsconfig.jsonnoEmitOnError中的选项,或构建工具中的等效选项。
You should aim for zero errors when you commit code, lest you fall into the trap of having to remember what is an expected or unexpected error. If you want to disable output on errors, you can use the noEmitOnError option in tsconfig.json, or the equivalent in your build tool.
You may be tempted to write code like this:
interfaceSquare{width:number;}interfaceRectangleextendsSquare{height:number;}typeShape=Square|Rectangle;functioncalculateArea(shape:Shape){if(shapeinstanceofRectangle){// ~~~~~~~~~ 'Rectangle' only refers to a type,// but is being used as a value herereturnshape.width*shape.height;// ~~~~~~ Property 'height' does not exist// on type 'Shape'}else{returnshape.width*shape.width;}}
interfaceSquare{width:number;}interfaceRectangleextendsSquare{height:number;}typeShape=Square|Rectangle;functioncalculateArea(shape:Shape){if(shapeinstanceofRectangle){// ~~~~~~~~~ 'Rectangle' only refers to a type,// but is being used as a value herereturnshape.width*shape.height;// ~~~~~~ Property 'height' does not exist// on type 'Shape'}else{returnshape.width*shape.width;}}
检查instanceof发生在运行时,但它Rectangle是一种类型,因此它不会影响代码的运行时行为。TypeScript 类型是“可擦除的”:编译为 JavaScript 的一部分只是从代码中删除所有interfaces、 s 和类型注释。type
The instanceof check happens at runtime, but Rectangle is a type and so it cannot affect the runtime behavior of the code. TypeScript types are “erasable”: part of compilation to JavaScript is simply removing all the interfaces, types, and type annotations from your code.
要确定您正在处理的形状类型,您需要某种方法在运行时重建其类型。在这种情况下,您可以检查属性是否存在height:
To ascertain the type of shape you’re dealing with, you’ll need some way to reconstruct its type at runtime. In this case you can check for the presence of a height property:
functioncalculateArea(shape:Shape){if('height'inshape){shape;// Type is Rectanglereturnshape.width*shape.height;}else{shape;// Type is Squarereturnshape.width*shape.width;}}
functioncalculateArea(shape:Shape){if('height'inshape){shape;// Type is Rectanglereturnshape.width*shape.height;}else{shape;// Type is Squarereturnshape.width*shape.width;}}
这是可行的,因为属性检查仅涉及运行时可用的值,但仍允许类型检查器将shape的类型细化为Rectangle.
This works because the property check only involves values available at runtime, but still allows the type checker to refine shape’s type to Rectangle.
另一种方法是引入一个“标签”以在运行时可用的方式显式存储类型:
Another way would have been to introduce a “tag” to explicitly store the type in a way that’s available at runtime:
interfaceSquare{kind:'square';width:number;}interfaceRectangle{kind:'rectangle';height:number;width:number;}typeShape=Square|Rectangle;functioncalculateArea(shape:Shape){if(shape.kind==='rectangle'){shape;// Type is Rectanglereturnshape.width*shape.height;}else{shape;// Type is Squarereturnshape.width*shape.width;}}
interfaceSquare{kind:'square';width:number;}interfaceRectangle{kind:'rectangle';height:number;width:number;}typeShape=Square|Rectangle;functioncalculateArea(shape:Shape){if(shape.kind==='rectangle'){shape;// Type is Rectanglereturnshape.width*shape.height;}else{shape;// Type is Squarereturnshape.width*shape.width;}}
这 Shape此处的类型是“标记联合”的示例。因为它们使得在运行时恢复类型信息变得如此容易,所以标记联合在 TypeScript 中无处不在。
The Shape type here is an example of a “tagged union.” Because they make it so easy to recover type information at runtime, tagged unions are ubiquitous in TypeScript.
一些构造同时引入类型(在运行时不可用)和值(在运行时可用)。关键字class就是其中之一。Making Squareand Rectangleclasses 是另一种修复错误的方法:
Some constructs introduce both a type (which is not available at runtime) and a value (which is). The class keyword is one of these. Making Square and Rectangle classes would have been another way to fix the error:
classSquare{constructor(publicwidth:number){}}classRectangleextendsSquare{constructor(publicwidth:number,publicheight:number){super(width);}}typeShape=Square|Rectangle;functioncalculateArea(shape:Shape){if(shapeinstanceofRectangle){shape;// Type is Rectanglereturnshape.width*shape.height;}else{shape;// Type is Squarereturnshape.width*shape.width;// OK}}
classSquare{constructor(publicwidth:number){}}classRectangleextendsSquare{constructor(publicwidth:number,publicheight:number){super(width);}}typeShape=Square|Rectangle;functioncalculateArea(shape:Shape){if(shapeinstanceofRectangle){shape;// Type is Rectanglereturnshape.width*shape.height;}else{shape;// Type is Squarereturnshape.width*shape.width;// OK}}
这是可行的,因为class Rectangle同时引入了类型和值,而interface只引入了类型。
This works because class Rectangle introduces both a type and a value, whereas interface only introduced a type.
inRectangle指type Shape = Square | Rectangle的是类型,而Rectangleinshape instanceof Rectangle指的是值。这种区别对理解很重要,但可能非常微妙。请参阅第 8 项。
The Rectangle in type Shape = Square | Rectangle refers to the type, but the Rectangle in shape instanceof Rectangle refers to the value. This distinction is important to understand but can be quite subtle. See Item 8.
认为您有一个可以是字符串或数字的值,并且您希望对其进行规范化,以便它始终是一个数字。这是类型检查器接受的错误尝试:
Suppose you have a value that could be a string or a number and you’d like to normalize it so that it’s always a number. Here’s a misguided attempt that the type checker accepts:
functionasNumber(val:number|string):number{returnvalasnumber;}
functionasNumber(val:number|string):number{returnvalasnumber;}
查看生成的 JavaScript 可以清楚地了解此函数的实际作用:
Looking at the generated JavaScript makes it clear what this function really does:
functionasNumber(val){returnval;}
functionasNumber(val){returnval;}
没有任何转换正在进行。这as number是一种类型操作,因此它不会影响代码的运行时行为。要规范化该值,您需要检查其运行时类型并使用 JavaScript 结构进行转换:
There is no conversion going on whatsoever. The as number is a type operation, so it cannot affect the runtime behavior of your code. To normalize the value you’ll need to check its runtime type and do the conversion using JavaScript constructs:
functionasNumber(val:number|string):number{returntypeof(val)==='string'?Number(val):val;}
functionasNumber(val:number|string):number{returntypeof(val)==='string'?Number(val):val;}
(as number是类型断言。有关何时适合使用它们的更多信息,请参阅第 9 项。)
(as number is a type assertion. For more on when it’s appropriate to use these, see Item 9.)
Could this function ever hit the final console.log?
functionsetLightSwitch(value:boolean){switch(value){casetrue:turnLightOn();break;casefalse:turnLightOff();break;default:console.log(`I'm afraid I can't do that.`);}}
functionsetLightSwitch(value:boolean){switch(value){casetrue:turnLightOn();break;casefalse:turnLightOff();break;default:console.log(`I'm afraid I can't do that.`);}}
TypeScript 通常会标记死代码,但它不会抱怨这一点,即使有这个strict选项。你怎么能打这个分支?
TypeScript usually flags dead code, but it does not complain about this, even with the strict option. How could you hit this branch?
这关键是要记住这boolean是声明的类型。因为它是 TypeScript 类型,所以它会在运行时消失。在 JavaScript 代码中,用户可能会无意中使用setLightSwitch类似"ON".
The key is to remember that boolean is the declared type. Because it is a TypeScript type, it goes away at runtime. In JavaScript code, a user might inadvertently call setLightSwitch with a value like "ON".
也有一些方法可以在纯 TypeScript 中触发此代码路径。也许该函数是使用来自网络调用的值调用的:
There are ways to trigger this code path in pure TypeScript, too. Perhaps the function is called with a value which comes from a network call:
interfaceLightApiResponse{lightSwitchValue:boolean;}asyncfunctionsetLight() {constresponse=awaitfetch('/light');constresult:LightApiResponse=awaitresponse.json();setLightSwitch(result.lightSwitchValue);}
interfaceLightApiResponse{lightSwitchValue:boolean;}asyncfunctionsetLight() {constresponse=awaitfetch('/light');constresult:LightApiResponse=awaitresponse.json();setLightSwitch(result.lightSwitchValue);}
您已经声明请求的结果/light是LightApiResponse,但没有任何强制执行。如果您误解了 API 并且lightSwitchValue实际上是一个string,那么将在运行时传递一个字符串setLightSwitch。或者 API 在您部署后发生了变化。
You’ve declared that the result of the /light request is LightApiResponse, but nothing enforces this. If you misunderstood the API and lightSwitchValue is really a string, then a string will be passed to setLightSwitch at runtime. Or perhaps the API changed after you deployed.
当您的运行时类型与声明的类型不匹配时,TypeScript 会变得非常混乱,您应该尽可能避免这种情况。但请注意,值可能具有您声明的类型以外的类型。
TypeScript can get quite confusing when your runtime types don’t match the declared types, and this is a situation you should avoid whenever you can. But be aware that it’s possible for a value to have types other than the ones you’ve declared.
语言像 C++ 一样,允许您定义一个函数的多个版本,它们仅在参数类型上有所不同。这称为“函数重载”。因为代码的运行时行为独立于它的 TypeScript 类型,所以这种构造在 TypeScript 中是不可能的:
Languages like C++ allow you to define multiple versions of a function that differ only in the types of their parameters. This is called “function overloading.” Because the runtime behavior of your code is independent of its TypeScript types, this construct isn’t possible in TypeScript:
functionadd(a::::number,b_number){returna+b;}// ~~~ Duplicate function implementationfunctionadd(a_string,b_string){returna+b;}// ~~~ Duplicate function implementation
functionadd(a:number,b:number){returna+b;}// ~~~ Duplicate function implementationfunctionadd(a:string,b:string){returna+b;}// ~~~ Duplicate function implementation
TypeScript确实提供了重载函数的工具,但它完全在类型级别运行。您可以为一个函数提供多个声明,但只能提供一个实现:
TypeScript does provide a facility for overloading functions, but it operates entirely at the type level. You can provide multiple declarations for a function, but only a single implementation:
functionadd(a::::number,b_number):number;functionadd(a_string,b_string):string;functionadd(a,b){returna+b;}constthree=add(1,2);// Type is numberconsttwelve=add('1','2');// Type is string
functionadd(a:number,b:number):number;functionadd(a:string,b:string):string;functionadd(a,b){returna+b;}constthree=add(1,2);// Type is numberconsttwelve=add('1','2');// Type is string
的前两个声明add只提供类型信息。当 TypeScript 生成 JavaScript 输出时,它们将被删除,只保留实现。(如果您使用这种重载方式,请先查看第 50 项。需要注意一些细微之处。)
The first two declarations of add only provide type information. When TypeScript produces JavaScript output, they are removed, and only the implementation remains. (If you use this style of overloading, take a look at Item 50 first. There are some subtleties to be aware of.)
因为类型和类型操作在生成 JavaScript 时被删除,它们不会对运行时性能产生影响。TypeScript 的静态类型是真正的零成本。下次有人以运行时开销作为不使用 TypeScript 的理由时,你就会确切地知道他们对这个声明的测试有多好!
Because types and type operations are erased when you generate JavaScript, they cannot have an effect on runtime performance. TypeScript’s static types are truly zero cost. The next time someone offers runtime overhead as a reason to not use TypeScript, you’ll know exactly how well they’ve tested this claim!
对此有两个警告:
There are two caveats to this:
虽然没有运行时开销,但 TypeScript 编译器会引入构建时间开销。TypeScript 团队非常重视编译器性能,编译通常非常快,尤其是增量构建。如果开销变得很大,你的构建工具可能有一个“transpile only”选项来跳过类型检查。
While there is no runtime overhead, the TypeScript compiler will introduce build time overhead. The TypeScript team takes compiler performance seriously and compilation is usually quite fast, especially for incremental builds. If the overhead becomes significant, your build tool may have a “transpile only” option to skip the type checking.
TypeScript 为支持较旧的运行时而发出的代码可能会产生与本机实现相比的性能开销。例如,如果您使用生成器函数和target ES5,它早于生成器,然后tsc将发出一些帮助代码来使事情正常进行。与生成器的本机实现相比,这可能会有一些开销。在任何情况下,这都与发射目标和语言级别有关,并且仍然独立于类型。
The code that TypeScript emits to support older runtimes may incur a performance overhead vs. native implementations. For example, if you use generator functions and target ES5, which predates generators, then tsc will emit some helper code to make things work. This may have some overhead vs. a native implementation of generators. In any case, this has to do with the emit target and language levels and is still independent of the types.
代码生成独立于类型系统。这意味着 TypeScript 类型不会影响代码的运行时行为或性能。
Code generation is independent of the type system. This means that TypeScript types cannot affect the runtime behavior or performance of your code.
具有类型错误的程序可能会生成代码(“编译”)。
It is possible for a program with type errors to produce code (“compile”).
TypeScript 类型在运行时不可用。要在运行时查询一个类型,您需要一些方法来重建它。标记工会和属性检查是执行此操作的常用方法。某些结构(例如class)同时引入了 TypeScript 类型和在运行时可用的值。
TypeScript types are not available at runtime. To query a type at runtime, you need some way to reconstruct it. Tagged unions and property checking are common ways to do this. Some constructs, such as class, introduce both a TypeScript type and a value that is available at runtime.
JavaScript本质上是鸭子类型的:如果您向函数传递一个具有所有正确属性的值,它不会关心您如何获得该值。它只会使用它。(“If it walks like a duck and talks like a duck...”)TypeScript 对这种行为进行建模,它有时会导致令人惊讶的结果,因为类型检查器对类型的理解可能比你想象的更广泛。很好地掌握结构类型将帮助您理解错误和非错误,并帮助您编写更健壮的代码。
JavaScript is inherently duck typed: if you pass a function a value with all the right properties, it won’t care how you made the value. It will just use it. (“If it walks like a duck and talks like a duck…”) TypeScript models this behavior, and it can sometimes lead to surprising results because the type checker’s understanding of a type may be broader than what you had in mind. Having a good grasp of structural typing will help you make sense of errors and non-errors and help you write more robust code.
假设您正在处理一个物理库并且有一个 2D 矢量类型:
Say you’re working on a physics library and have a 2D vector type:
interfaceVector2D{x:number;y:number;}
interfaceVector2D{x:number;y:number;}
你写了一个函数来计算它的长度:
You write a function to calculate its length:
functioncalculateLength(v:Vector2D){returnMath.sqrt(v.x*v.x+v.y*v.y);}
functioncalculateLength(v:Vector2D){returnMath.sqrt(v.x*v.x+v.y*v.y);}
现在介绍命名向量的概念:
Now you introduce the notion of a named vector:
interfaceNamedVector{name:string;x:number;y:number;}
interfaceNamedVector{name:string;x:number;y:number;}
该calculateLength函数将与NamedVectors 一起使用,因为它们具有x和y属性,即numbers。TypeScript 足够聪明,可以解决这个问题:
The calculateLength function will work with NamedVectors because they have x and y properties, which are numbers. TypeScript is smart enough to figure this out:
constv:NamedVector={x:3,y:4,name:'Zee'};calculateLength(v);// OK, result is 5
constv:NamedVector={x:3,y:4,name:'Zee'};calculateLength(v);// OK, result is 5
Vector2D有趣的是,您从未声明和之间的关系NamedVector。而且您不必calculateLength为 s 编写 calculateLength的替代实现NamedVector。TypeScript 的类型系统正在模拟 JavaScript 的运行时行为(第 1 项)。它允许calculateLength用 a 调用,NamedVector因为它的结构与 兼容Vector2D。这就是术语“结构类型”的来源。
What is interesting is that you never declared the relationship between Vector2D and NamedVector. And you did not have to write an alternative implementation of calculateLength calculateLength for NamedVectors. TypeScript’s type system is modeling JavaScript’s runtime behavior (Item 1). It allowed calculateLength to be called with a NamedVector because its structure was compatible with Vector2D. This is where the term “structural typing” comes from.
但这也可能导致麻烦。假设您添加了一个 3D 矢量类型:
But this can also lead to trouble. Say you add a 3D vector type:
interfaceVector3D{x:number;y:number;z:number;}
interfaceVector3D{x:number;y:number;z:number;}
并编写一个函数来规范化它们(使它们的长度为 1):
and write a function to normalize them (make their length 1):
functionnormalize(v::::Vector3D){constlength=calculateLength(v);return{x_v.x/length,y_v.y/length,z_v.z/length,};}
functionnormalize(v:Vector3D){constlength=calculateLength(v);return{x:v.x/length,y:v.y/length,z:v.z/length,};}
如果你调用这个函数,你可能会得到比单位长度更长的东西:
If you call this function, you’re likely to get something longer than unit length:
>归一化({x: 3, y: 4, z: 5})
{ x: 0.6, y: 0.8, z: 1}> normalize({x: 3, y: 4, z: 5})
{ x: 0.6, y: 0.8, z: 1 }
那么到底出了什么问题,为什么 TypeScript 没有捕捉到错误呢?
So what went wrong and why didn’t TypeScript catch the error?
错误是calculateLength在 2D 向量上运行但normalize在 3D 向量上运行。所以z组件在规范化中被忽略。
The bug is that calculateLength operates on 2D vectors but normalize operates on 3D vectors. So the z component is ignored in the normalization.
也许更令人惊讶的是类型检查器没有发现这个问题。为什么允许calculateLength使用 3D 向量调用,尽管它的类型声明说它采用 2D 向量?
What’s perhaps more surprising is that the type checker does not catch this issue. Why are you allowed to call calculateLength with a 3D vector, despite its type declaration saying that it takes 2D vectors?
对命名向量如此有效的做法在这里适得其反。calculateLength使用对象调用{x, y, z}不会引发错误。所以类型检查器也没有抱怨,而且这种行为导致了错误。(如果你希望这是一个错误,你有一些选择。我们将在条款 37中返回到这个例子。)
What worked so well with named vectors has backfired here. Calling calculateLength with an {x, y, z} object doesn’t throw an error. So the type checker doesn’t complain, either, and this behavior has led to a bug. (If you want this to be an error, you have some options. We’ll return to this example in Item 37.)
当您编写函数时,很容易想象它们将使用具有您声明的属性的参数调用,而没有其他属性。这被称为“密封”或“精确”类型,它不能在 TypeScript 的类型系统中表达。不管喜欢与否,您的类型是“开放的”。
As you write functions, it’s easy to imagine that they will be called with arguments having the properties you’ve declared and no others. This is known as a “sealed” or “precise” type, and it cannot be expressed in TypeScript’s type system. Like it or not, your types are “open.”
This can sometimes lead to surprises:
functioncalculateLengthL1(v:Vector3D){letlength=0;for(constaxisofObject.keys(v)){constcoord=v[axis];// ~~~~~~~ Element implicitly has an 'any' type because ...// 'string' can't be used to index type 'Vector3D'length+=Math.abs(coord);}returnlength;}
functioncalculateLengthL1(v:Vector3D){letlength=0;for(constaxisofObject.keys(v)){constcoord=v[axis];// ~~~~~~~ Element implicitly has an 'any' type because ...// 'string' can't be used to index type 'Vector3D'length+=Math.abs(coord);}returnlength;}
为什么这是一个错误?由于axis是 的键之一v,它是Vector3D,它应该是"x"、"y"或"z"。而且根据 的声明Vector3D,这些都是numbers,那么 的类型不应该coord是吗number?
Why is this an error? Since axis is one of the keys of v, which is a Vector3D, it should be either "x", "y", or "z". And according to the declaration of Vector3D, these are all numbers, so shouldn’t the type of coord be number?
这个错误是误报吗?不!TypeScript 抱怨是正确的。上一段中的逻辑假定 是Vector3D密封的并且没有其他属性。但它可以:
Is this error a false positive? No! TypeScript is correct to complain. The logic in the previous paragraph assumes that Vector3D is sealed and does not have other properties. But it could:
constvec3D={x:3,y:4,z:1,address:'123 Broadway'};calculateLengthL1(vec3D);// OK, returns NaN
constvec3D={x:3,y:4,z:1,address:'123 Broadway'};calculateLengthL1(vec3D);// OK, returns NaN
由于v可以想象具有任何属性,因此类型axis是string. TypeScript 没有理由相信这v[axis]是一个数字,因为正如您刚才看到的,它可能不是。遍历对象可能很难正确输入。我们将在条款 54中回到这个主题,但在这种情况下,没有循环的实现会更好:
Since v could conceivably have any properties, the type of axis is string. TypeScript has no reason to believe that v[axis] is a number because, as you just saw, it might not be. Iterating over objects can be tricky to type correctly. We’ll return to this topic in Item 54, but in this case an implementation without loops would be better:
functioncalculateLengthL1(v:Vector3D){returnMath.abs(v.x)+Math.abs(v.y)+Math.abs(v.z);}
functioncalculateLengthL1(v:Vector3D){returnMath.abs(v.x)+Math.abs(v.y)+Math.abs(v.z);}
结构类型也可能导致classes 的意外,它们在结构上比较可分配性:
Structural typing can also lead to surprises with classes, which are compared structurally for assignability:
classC{foo:string;constructor(foo:string){this.foo=foo;}}constc=newC('instance of C');constd:C={foo:'object literal'};// OK!
classC{foo:string;constructor(foo:string){this.foo=foo;}}constc=newC('instance of C');constd:C={foo:'object literal'};// OK!
为什么可以d分配给C?它有一个foo属性是string. 此外,它还有一个constructor(from Object.prototype) 可以用一个参数调用(尽管它通常用零调用)。所以结构匹配。这C如果您在的构造函数中有逻辑并编写一个假设它正在运行的函数,则可能会导致意外。这与 C++ 或 Java 等语言完全不同,其中声明类型的参数C保证它将是C它的一个或一个子类。
Why is d assignable to C? It has a foo property that is a string. In addition, it has a constructor (from Object.prototype) that can be called with one argument (though it is usually called with zero). So the structures match. This might lead to surprises if you have logic in C’s constructor and write a function that assumes it’s run. This is quite different from languages like C++ or Java, where declaring a parameter of type C guarantees that it will be either C or a subclass of it.
当您编写测试时,结构类型非常有用。假设您有一个对数据库运行查询并处理结果的函数:
Structural typing is beneficial when you’re writing tests. Say you have a function that runs a query on a database and processes the results:
interfaceAuthor{first::::::string;last_string;}functiongetAuthors(database_PostgresDB):Author[]{constauthorRows=database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);returnauthorRows.map(row=>({first_row[0],last_row[1]}));}
interfaceAuthor{first:string;last:string;}functiongetAuthors(database:PostgresDB):Author[]{constauthorRows=database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);returnauthorRows.map(row=>({first:row[0],last:row[1]}));}
要对此进行测试,您可以创建一个模拟PostgresDB. 但更好的方法是使用结构类型并定义更窄的接口:
To test this, you could create a mock PostgresDB. But a better approach is to use structural typing and define a narrower interface:
interfaceDB{runQuery:(sql::::string)=>any[];}functiongetAuthors(database_DB):Author[]{constauthorRows=database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);returnauthorRows.map(row=>({first_row[0],last_row[1]}));}
interfaceDB{runQuery:(sql:string)=>any[];}functiongetAuthors(database:DB):Author[]{constauthorRows=database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);returnauthorRows.map(row=>({first:row[0],last:row[1]}));}
你仍然可以在生产中通过getAuthorsaPostgresDB因为它有一个runQuery方法。由于结构类型,PostgresDB不需要说它实现了DB. TypeScript 会发现它确实如此。
You can still pass getAuthors a PostgresDB in production since it has a runQuery method. Because of structural typing, the PostgresDB doesn’t need to say that it implements DB. TypeScript will figure out that it does.
当您编写测试时,您可以传入一个更简单的对象:
When you write your tests, you can pass in a simpler object instead:
test('getAuthors',()=>{constauthors=getAuthors({runQuery(sql:string){return[['Toni','Morrison'],['Maya','Angelou']];}});expect(authors).toEqual([{first:'Toni',last:'Morrison'},{first:'Maya',last:'Angelou'}]);});
test('getAuthors',()=>{constauthors=getAuthors({runQuery(sql:string){return[['Toni','Morrison'],['Maya','Angelou']];}});expect(authors).toEqual([{first:'Toni',last:'Morrison'},{first:'Maya',last:'Angelou'}]);});
TypeScript 将验证我们的测试DB是否符合接口。而且您的测试不需要了解有关您的生产数据库的任何信息:不需要模拟库!通过引入抽象 ( DB),我们将逻辑(和测试)从特定实现 ( PostgresDB) 的细节中解放出来。
TypeScript will verify that our test DB conforms to the interface. And your tests don’t need to know anything about your production database: no mocking libraries necessary! By introducing an abstraction (DB), we’ve freed our logic (and tests) from the details of a specific implementation (PostgresDB).
结构类型的另一个优点是它可以干净利落地切断库之间的依赖关系。有关更多信息,请参阅条目 51。
Another advantage of structural typing is that it can cleanly sever dependencies between libraries. For more on this, see Item 51.
了解 JavaScript 是鸭子类型的,而 TypeScript 使用结构类型对此进行建模:可分配给接口的值可能具有超出类型声明中明确列出的属性。类型不是“密封的”。
Understand that JavaScript is duck typed and TypeScript uses structural typing to model this: values assignable to your interfaces might have properties beyond those explicitly listed in your type declarations. Types are not “sealed.”
请注意,类也遵循结构类型规则。您可能没有您期望的类的实例!
Be aware that classes also follow structural typing rules. You may not have an instance of the class you expect!
TypeScript 的类型系统是渐进的和可选的:渐进是因为你可以一点一点地向你的代码添加类型,可选是因为你可以随时禁用类型检查器。这些特性的关键是any类型:
TypeScript’s type system is gradual and optional: gradual because you can add types to your code bit by bit and optional because you can disable the type checker whenever you like. The key to these features is the any type:
letage:number;age='12';// ~~~ Type '"12"' is not assignable to type 'number'age='12'asany;// OK
letage:number;age='12';// ~~~ Type '"12"' is not assignable to type 'number'age='12'asany;// OK
类型检查器在这里抱怨是对的,但你可以通过输入as any. 当您开始使用 TypeScript 时,很容易使用any类型和type assertions ( as any) 当你不理解一个错误时,认为类型检查器不正确,或者只是不想花时间写出类型声明。在某些情况下,这可能没问题,但请注意,这any会消除使用 TypeScript 的许多优势。在使用它之前,您至少应该了解它的危险。
The type checker is right to complain here, but you can silence it just by typing as any. As you start using TypeScript, it’s tempting to use any types and type assertions (as any) when you don’t understand an error, think the type checker is incorrect, or simply don’t want to take the time to write out type declarations. In some cases this may be OK, but be aware that any eliminates many of the advantages of using TypeScript. You should at least understand its dangers before you use it.
在在前面的示例中,类型声明表明它age是一个number. 但是any让你string给它分配一个。类型检查器会相信它是一个number(毕竟那是你所说的),并且混乱将不会被捕获:
In the preceding example, the type declaration says that age is a number. But any lets you assign a string to it. The type checker will believe that it’s a number (that’s what you said, after all), and the chaos will go uncaught:
age+=1;// OK; at runtime, age is now "121"
age+=1;// OK; at runtime, age is now "121"
什么时候你写了一个函数,你就是在指定一个契约:如果调用者给你某种类型的输入,你就会产生某种类型的输出。但是使用类型any你可以打破这些契约:
When you write a function, you are specifying a contract: if the caller gives you a certain type of input, you’ll produce a certain type of output. But with an any type you can break these contracts:
functioncalculateAge(birthDate:Date):number{// ...}letbirthDate:any='1990-01-19';calculateAge(birthDate);// OK
functioncalculateAge(birthDate:Date):number{// ...}letbirthDate:any='1990-01-19';calculateAge(birthDate);// OK
出生日期参数应该是 a Date,而不是 a string。类型any已经让你违约了calculateAge。这可能特别有问题,因为 JavaScript 通常愿意在类型之间隐式转换。遗嘱string有时会在number预期的地方发挥作用,但在其他情况下却会破裂。
The birth date parameter should be a Date, not a string. The any type has let you break the contract of calculateAge. This can be particularly problematic because JavaScript is often willing to implicitly convert between types. A string will sometimes work where a number is expected, only to break in other circumstances.
什么时候符号有类型,TypeScript 语言服务能够提供智能自动完成和上下文文档(如图1-3所示)。
When a symbol has a type, the TypeScript language services are able to provide intelligent autocomplete and contextual documentation (as shown in Figure 1-3).
但对于具有any类型的符号,你就得靠自己了(图 1-4)。
but for symbols with an any type, you’re on your own (Figure 1-4).
重命名是另一种此类服务。如果你有一个 Person 类型和函数来格式化一个人的名字:
Renaming is another such service. If you have a Person type and functions to format a person’s name:
interfacePerson{first:string;last:string;}constformatName=(p:Person)=>`${p.first}${p.last}`;constformatNameAny=(p:any)=>`${p.first}${p.last}`;
interfacePerson{first:string;last:string;}constformatName=(p:Person)=>`${p.first}${p.last}`;constformatNameAny=(p:any)=>`${p.first}${p.last}`;
然后你可以first在你的编辑器中选择“Rename Symbol”,把它改成firstName(见图1-5和1-6)。
then you can select first in your editor, choose “Rename Symbol,” and change it to firstName (see Figures 1-5 and 1-6).
这会更改formatName功能但不会更改any版本:
This changes the formatName function but not the any version:
interfacePerson{firstName::::string;last_string;}constformatName=(p_Person)=>`${p.firstName}${p.last}`;constformatNameAny=(p_any)=>`${p.first}${p.last}`;
interfacePerson{firstName:string;last:string;}constformatName=(p:Person)=>`${p.firstName}${p.last}`;constformatNameAny=(p:any)=>`${p.first}${p.last}`;
TypeScript 的座右铭是“可扩展的 JavaScript”。“规模”的一个关键部分是语言服务,它是 TypeScript 体验的核心部分(请参阅第 6 项)。失去它们将导致生产力下降,不仅对您如此,对使用您的代码的其他人也是如此。
TypeScript’s motto is “JavaScript that scales.” A key part of “scales” is the language services, which are a core part of the TypeScript experience (see Item 6). Losing them will lead to a loss in productivity, not just for you but for everyone else working with your code.
认为您正在构建一个 Web 应用程序,用户可以在其中选择某种项目。您的组件之一可能有onSelectItem回调。为 Item 编写类型似乎很麻烦,因此您只需使用它any作为替代:
Suppose you’re building a web application in which users can select some sort of item. One of your components might have an onSelectItem callback. Writing a type for an Item seems like a hassle, so you just use any as a stand-in:
interfaceComponentProps{onSelectItem:(item:any)=>void;}
interfaceComponentProps{onSelectItem:(item:any)=>void;}
这是管理该组件的代码:
Here’s code that manages that component:
functionrenderSelector(props:ComponentProps){/* ... */}letselectedId:number=0;functionhandleSelectItem(item:any){selectedId=item.id;}renderSelector({onSelectItem:handleSelectItem});
functionrenderSelector(props:ComponentProps){/* ... */}letselectedId:number=0;functionhandleSelectItem(item:any){selectedId=item.id;}renderSelector({onSelectItem:handleSelectItem});
稍后您重新设计选择器,使其更难将整个item对象传递给onSelectItem. 但这没什么大不了的,因为您只需要 ID。您更改签名ComponentProps:
Later you rework the selector in a way that makes it harder to pass the whole item object through to onSelectItem. But that’s no big deal since you just need the ID. You change the signature in ComponentProps:
interfaceComponentProps{onSelectItem:(id:number)=>void;}
interfaceComponentProps{onSelectItem:(id:number)=>void;}
你更新了组件,一切都通过了类型检查器。胜利!
You update the component and everything passes the type checker. Victory!
……是吗?handleSelectItem接受一个any参数,所以它对 Item 和 ID 一样满意。尽管通过了类型检查器,但它会产生运行时异常。如果您使用了更具体的类型,这将被类型检查器捕获。
…or is it? handleSelectItem takes an any parameter, so it’s just as happy with an Item as it is with an ID. It produces a runtime exception, despite passing the type checker. Had you used a more specific type, this would have been caught by the type checker.
这像应用程序状态这样的复杂对象的类型定义可能会很长。与其为页面状态中的数十个属性写出类型,不如直接使用一个any类型并完成它。
The type definition for complex objects like your application state can get quite long. Rather than writing out types for the dozens of properties in your page’s state, you may be tempted to just use an any type and be done with it.
由于本项目中列出的所有原因,这是有问题的。但这也有问题,因为它隐藏了您的状态设计。正如第 4 章所解释的,良好的类型设计对于编写干净、正确且易于理解的代码至关重要。对于any类型,您的类型设计是隐式的。这使得很难知道设计是否是一个好设计,甚至设计到底是什么。如果您要求同事审查更改,他们将不得不重建您是否以及如何更改应用程序状态。最好写出来让大家看看。
This is problematic for all the reasons listed in this item. But it’s also problematic because it hides the design of your state. As Chapter 4 explains, good type design is essential for writing clean, correct, and understandable code. With an any type, your type design is implicit. This makes it hard to know whether the design is a good one, or even what the design is at all. If you ask a coworker to review a change, they’ll have to reconstruct whether and how you changed the application state. Better to write it out for everyone to see.
每次你犯了一个错误并且类型检查器发现了它,它都会增强你对类型系统的信心。但是,当您在运行时看到类型错误时,这种信心就会受到打击。如果您在更大的团队中引入 TypeScript,这可能会让您的同事质疑 TypeScript 是否值得付出努力。any类型通常是这些未捕获错误的来源。
Every time you make a mistake and the type checker catches it, it boosts your confidence in the type system. But when you see a type error at runtime, that confidence takes a hit. If you’re introducing TypeScript on a larger team, this might make your coworkers question whether TypeScript is worth the effort. any types are often the source of these uncaught errors.
TypeScript 旨在让您的生活更轻松,但与无类型的 JavaScript 相比,具有多种any类型的 TypeScript 更难使用,因为您必须修复类型错误并仍然在脑海中跟踪真实类型。当您的类型与现实相符时,您就可以摆脱必须在脑海中保留类型信息的负担。TypeScript 会为你跟踪它。
TypeScript aims to make your life easier, but TypeScript with lots of any types can be harder to work with than untyped JavaScript because you have to fix type errors and still keep track of the real types in your head. When your types match reality, it frees you from the burden of having to keep type information in your head. TypeScript will keep track of it for you.
对于必须使用 的时候any,有更好和更坏的方法。有关如何限制缺点的更多信息any,请参阅第 5 章。
For the times when you must use any, there are better and worse ways to do it. For much more on how to limit the downsides of any, see Chapter 5.
TypeScript 生成代码(第 3 项),但类型系统是主要事件。这就是您使用该语言的原因!
TypeScript generates code (Item 3), but the type system is the main event. This is why you’re using the language!
本章将带您了解 TypeScript 类型系统的具体细节:如何思考它、如何使用它、您需要做出的选择以及您应该避免的特性。TypeScript 的类型系统非常强大,能够表达您可能不希望类型系统能够表达的东西。本章中的项目将为您打下坚实的基础,让您在编写 TypeScript 和阅读本书的其余部分时打下坚实的基础。
This chapter walks you through the nuts and bolts of TypeScript’s type system: how to think about it, how to use it, choices you’ll need to make, and features you should avoid. TypeScript’s type system is surprisingly powerful and able to express things you might not expect a type system to be able to. The items in this chapter will give you a solid foundation to build upon as you write TypeScript and read the rest of this book.
什么时候你安装 TypeScript,你会得到两个可执行文件:
When you install TypeScript, you get two executables:
tsc, TypeScript 编译器
tsc, the TypeScript compiler
tsserver, TypeScript 独立服务器
tsserver, the TypeScript standalone server
你是更有可能直接运行 TypeScript 编译器,但服务器同样重要,因为它提供语言服务。这些包括自动完成、检查、导航和重构。您通常通过编辑器使用这些服务。如果您的未配置为提供它们,那么您就错过了!像自动完成这样的服务是让 TypeScript 使用起来如此愉快的原因之一。但除了方便之外,您的编辑器是构建和测试您的类型系统知识的最佳场所。这将帮助您对 TypeScript 何时能够推断类型建立直觉,这是编写紧凑、惯用代码的关键(请参阅条目 19)。
You’re much more likely to run the TypeScript compiler directly, but the server is every bit as important because it provides language services. These include autocomplete, inspection, navigation, and refactoring. You typically use these services through your editor. If yours isn’t configured to provide them, then you’re missing out! Services like autocomplete are one of the things that make TypeScript such a joy to use. But beyond convenience, your editor is the best place to build and test your knowledge of the type system. This will help you build an intuition for when TypeScript is able to infer types, which is key to writing compact, idiomatic code (see Item 19).
细节因编辑器而异,但您通常可以将鼠标悬停在符号上以查看 TypeScript 认为其类型的内容(参见图 2-1)。
The details will vary from editor to editor, but you can generally mouse over a symbol to see what TypeScript considers its type (see Figure 2-1).
你没有number在这里写,但 TypeScript 能够根据值 10 计算出来。
You didn’t write number here, but TypeScript was able to figure it out based on the value 10.
你也可以检查函数,如图2-2所示。
You can also inspect functions, as shown in Figure 2-2.
值得注意的信息是返回类型的推断值,number。如果这与您的期望不符,您应该添加一个类型声明并找出差异(请参阅第 9 项)。
The noteworthy bit of information is the inferred value for the return type, number. If this does not match your expectation, you should add a type declaration and track down the discrepancy (see Item 9).
在任何给定点查看 TypeScript 对变量类型的理解对于围绕扩大(第 21 项)和缩小(第 22 项)建立直觉至关重要。查看条件分支中变量类型的变化是建立对类型系统信心的好方法(见图2-3)。
Seeing TypeScript’s understanding of a variable’s type at any given point is essential for building an intuition around widening (Item 21) and narrowing (Item 22). Seeing the type of a variable change in the branch of a conditional is a tremendous way to build confidence in the type system (see Figure 2-3).
您可以检查更大对象中的各个属性,以查看 TypeScript 对它们的推断(参见图 2-4)。
You can inspect individual properties in a larger object to see what TypeScript has inferred about them (see Figure 2-4).
如果您的意图是x成为元组类型 ( [number, number, number]),则需要类型注释。
If your intention was for x to be a tuple type ([number, number, number]), then a type annotation will be required.
要查看操作链中间的推断泛型类型,请检查方法名称(如图2-5所示)。
To see inferred generic types in the middle of a chain of operations, inspect the method name (as shown in Figure 2-5).
表示Array<string>TypeScript 理解split生成的字符串数组。虽然在这种情况下几乎没有歧义,但事实证明这些信息对于编写和调试长链函数调用至关重要。
The Array<string> indicates that TypeScript understands that split produced an array of strings. While there was little ambiguity in this case, this information can prove essential in writing and debugging long chains of function calls.
在编辑器中查看类型错误也是了解类型系统细微差别的好方法。例如,此函数尝试通过其 ID 获取一个HTMLElement,或返回一个默认值。TypeScript 标记了两个错误:
Seeing type errors in your editor can also be a great way to learn the nuances of the type system. For example, this function tries to get an HTMLElement by its ID, or return a default one. TypeScript flags two errors:
functiongetElement(elOrId:string|HTMLElement|null):HTMLElement{if(typeofelOrId==='object'){returnelOrId;// ~~~~~~~~~~~~~~ 'HTMLElement | null' is not assignable to 'HTMLElement'}elseif(elOrId===null){returndocument.body;}else{constel=document.getElementById(elOrId);returnel;// ~~~~~~~~~~ 'HTMLElement | null' is not assignable to 'HTMLElement'}}
functiongetElement(elOrId:string|HTMLElement|null):HTMLElement{if(typeofelOrId==='object'){returnelOrId;// ~~~~~~~~~~~~~~ 'HTMLElement | null' is not assignable to 'HTMLElement'}elseif(elOrId===null){returndocument.body;}else{constel=document.getElementById(elOrId);returnel;// ~~~~~~~~~~ 'HTMLElement | null' is not assignable to 'HTMLElement'}}
语句的第一个分支的目的if是过滤到对象,即HTMLElements。但奇怪的是,在 JavaScript 中typeof null是"object",所以elOrId仍然可以null在那个分支中。您可以通过null先检查来解决此问题。第二个错误是因为document.getElementByIdcan return null,所以你也需要处理这种情况,也许通过抛出异常。
The intent in the first branch of the if statement was to filter down to just the objects, namely, the HTMLElements. But oddly enough, in JavaScript typeof null is "object", so elOrId could still be null in that branch. You can fix this by putting the null check first. The second error is because document.getElementById can return null, so you need to handle that case as well, perhaps by throwing an exception.
语言服务还可以帮助您浏览库和类型声明。假设您在代码中看到对函数的调用fetch,并想了解更多信息。您的编辑器应提供“转到定义”选项。在我的系统中,它看起来像图 2-6中那样。
Language services can also help you navigate through libraries and type declarations. Suppose you see a call to the fetch function in code and want to learn more about it. Your editor should provide a “Go to Definition” option. In mine it looks like it does in Figure 2-6.
选择此选项会将您带入lib.dom.d.tsTypeScript 为 DOM 包含的类型声明:
Selecting this option takes you into lib.dom.d.ts, the type declarations which TypeScript includes for the DOM:
declarefunctionfetch(input:RequestInfo,init?:RequestInit):Promise<Response>;
declarefunctionfetch(input:RequestInfo,init?:RequestInit):Promise<Response>;
您可以看到fetch返回 aPromise并接受两个参数。点击通过将RequestInfo您带到这里:
You can see that fetch returns a Promise and takes two arguments. Clicking through on RequestInfo brings you here:
typeRequestInfo=Request|string;
typeRequestInfo=Request|string;
从那里你可以去Request:
from which you can go to Request:
declarevarRequest:{prototype:Request;new(input:RequestInfo,init?:RequestInit):Request;};
declarevarRequest:{prototype:Request;new(input:RequestInfo,init?:RequestInit):Request;};
在这里您可以看到类型Request和值是分开建模的(参见条目 8)。你RequestInfo已经看到了。单击显示RequestInit可用于构建的所有内容Request:
Here you can see that the Request type and value are being modeled separately (see Item 8). You’ve seen RequestInfo already. Clicking through on RequestInit shows everything you can use to construct a Request:
interfaceRequestInit{body?::::BodyInit|null;cache?_RequestCache;credentials?_RequestCredentials;headers?_HeadersInit;// ...}
interfaceRequestInit{body?:BodyInit|null;cache?:RequestCache;credentials?:RequestCredentials;headers?:HeadersInit;// ...}
您可以在此处遵循更多类型,但您明白了。类型声明一开始可能难以阅读,但它们是了解 TypeScript 可以做什么、您正在使用的库如何建模以及如何调试错误的绝佳方式。有关类型声明的更多信息,请参阅第 6 章。
There are many more types you could follow here, but you get the idea. Type declarations can be challenging to read at first, but they’re an excellent way to see what can be done with TypeScript, how the library you’re using is modeled, and how you might debug errors. For much more on type declarations, see Chapter 6.
通过使用可以使用它们的编辑器来利用 TypeScript 语言服务。
Take advantage of the TypeScript language services by using an editor that can use them.
使用您的编辑器来构建关于类型系统如何工作以及 TypeScript 如何推断类型的直觉。
Use your editor to build an intuition for how the type system works and how TypeScript infers types.
Know how to jump into type declaration files to see how they model behavior.
在在运行时,每个变量都有一个从 JavaScript 的值域中选择的值。有许多可能的值,包括:
At runtime, every variable has a single value chosen from JavaScript’s universe of values. There are many possible values, including:
42
42
null
null
undefined
undefined
'Canada'
'Canada'
{animal: 'Whale', weight_lbs: 40_000}
{animal: 'Whale', weight_lbs: 40_000}
/regex/
/regex/
new HTMLButtonElement
new HTMLButtonElement
(x, y) => x + y
(x, y) => x + y
但是在你的代码运行之前,当 TypeScript 检查它是否有错误时,它只有一个type。最好将其视为一组可能的值。这个集合被称为类型的域。例如,您可以将number类型视为所有数字值的集合。42并且-37.25在其中,但'Canada'不是。取决于strictNullChecks,null并且undefined可能是也可能不是集合的一部分。
But before your code runs, when TypeScript is checking it for errors, it just has a type. This is best thought of as a set of possible values. This set is known as the domain of the type. For instance, you can think of the number type as the set of all number values. 42 and -37.25 are in it, but 'Canada' is not. Depending on strictNullChecks, null and undefined may or may not be part of the set.
最小的集合是空集,它不包含任何值。它对应于neverTypeScript 中的类型。因为它的域是空的,所以没有值可以分配给具有以下never类型的变量:
The smallest set is the empty set, which contains no values. It corresponds to the never type in TypeScript. Because its domain is empty, no values are assignable to a variable with a never type:
constx:never=12;// ~ Type '12' is not assignable to type 'never'
constx:never=12;// ~ Type '12' is not assignable to type 'never'
下一个最小的集合是那些包含单个值的集合。这些对应于 TypeScript 中的文字类型,也称为单元类型:
The next smallest sets are those which contain single values. These correspond to literal types in TypeScript, also known as unit types:
typeA='A';typeB='B';typeTwelve=12;
typeA='A';typeB='B';typeTwelve=12;
要形成具有两个或三个值的类型,您可以合并单元类型:
To form types with two or three values, you can union unit types:
typeAB='A'|'B';typeAB12='A'|'B'|12;
typeAB='A'|'B';typeAB12='A'|'B'|12;
等等。联合类型对应于值集的联合。
and so on. Union types correspond to unions of sets of values.
这“assignable”一词出现在许多 TypeScript 错误中。在值集的上下文中,它表示“成员”(对于值和类型之间的关系)或“子集”(对于两种类型之间的关系):
The word “assignable” appears in many TypeScript errors. In the context of sets of values, it means either “member of” (for a relationship between a value and a type) or “subset of” (for a relationship between two types):
consta:AB='A';// OK, value 'A' is a member of the set {'A', 'B'}constc:AB='C';// ~ Type '"C"' is not assignable to type 'AB'
consta:AB='A';// OK, value 'A' is a member of the set {'A', 'B'}constc:AB='C';// ~ Type '"C"' is not assignable to type 'AB'
类型"C"是单位类型。它的域由单个值组成"C"。这不是域的子集(由值和AB组成),因此这是一个错误。归根结底,类型检查器所做的几乎所有工作都是测试一组是否是另一组的子集:"A""B"
The type "C" is a unit type. Its domain consists of the single value "C". This is not a subset of the domain of AB (which consists of the values "A" and "B"), so this is an error. At the end of the day, almost all the type checker is doing is testing whether one set is a subset of another:
// OK, {"A", "B"} is a subset of {"A", "B"}:constab:AB=Math.random()<0.5?'A':'B';constab12:AB12=ab;// OK, {"A", "B"} is a subset of {"A", "B", 12}declarelettwelve:AB12;constback:AB=twelve;// ~~~~ Type 'AB12' is not assignable to type 'AB'// Type '12' is not assignable to type 'AB'
// OK, {"A", "B"} is a subset of {"A", "B"}:constab:AB=Math.random()<0.5?'A':'B';constab12:AB12=ab;// OK, {"A", "B"} is a subset of {"A", "B", 12}declarelettwelve:AB12;constback:AB=twelve;// ~~~~ Type 'AB12' is not assignable to type 'AB'// Type '12' is not assignable to type 'AB'
这些类型的集合很容易推理,因为它们是有限的。但您在实践中使用的大多数类型都具有无限域。关于这些的推理可能更难。您可以将它们视为建设性地构建:
The sets for these types are easy to reason about because they are finite. But most types that you work with in practice have infinite domains. Reasoning about these can be harder. You can think of them as either being built constructively:
typeInt=1|2|3|4|5// | ...
typeInt=1|2|3|4|5// | ...
或者通过描述他们的成员:
or by describing their members:
interfaceIdentified{id:string;}
interfaceIdentified{id:string;}
将此接口视为对其类型域中值的描述。该值是否具有id可分配给(的成员)的属性string?然后是一个Identifiable.
Think of this interface as a description of the values in the domain of its type. Does the value have an id property whose value is assignable to (a member of) string? Then it’s an Identifiable.
这就是它所说的全部。正如第 4 项所解释的,TypeScript 的结构类型规则意味着该值也可以具有其他属性。它甚至可以被调用!这个事实有时会被过多的属性检查所掩盖(见条款 11)。
That’s all it says. As Item 4 explained, TypeScript’s structural typing rules mean that the value could have other properties, too. It could even be callable! This fact can sometimes be obscured by excess property checking (see Item 11).
将类型视为一组值可以帮助您推理对它们的操作。例如:
Thinking of types as sets of values helps you reason about operations on them. For example:
interfacePerson{name:string;}interfaceLifespan{birth:Date;death?:Date;}typePersonSpan=Person&Lifespan;
interfacePerson{name:string;}interfaceLifespan{birth:Date;death?:Date;}typePersonSpan=Person&Lifespan;
这 &运算符计算两种类型的交集。什么样的值属于该PersonSpan类型?乍一看,Person和Lifespan接口没有共同的属性,因此您可能认为它是空集(即类型never)。但是类型操作适用于值集(类型的域),而不适用于接口中的属性。请记住,具有附加属性的值仍然属于一个类型。所以一个值同时 具有Person 和 Lifespan的属性属于交集类型:
The & operator computes the intersection of two types. What sorts of values belong to the PersonSpan type? On first glance the Person and Lifespan interfaces have no properties in common, so you might expect it to be the empty set (i.e., the never type). But type operations apply to the sets of values (the domain of the type), not to the properties in the interface. And remember that values with additional properties still belong to a type. So a value that has the properties of both Person and Lifespan will belong to the intersection type:
constps:PersonSpan={name:'Alan Turing',birth:newDate('1912/06/23'),death:newDate('1954/06/07'),};// OK
constps:PersonSpan={name:'Alan Turing',birth:newDate('1912/06/23'),death:newDate('1954/06/07'),};// OK
当然,一个值可以拥有不止这三个属性并且仍然属于该类型!一般规则是交集类型中的值包含其每个成分中的属性的并集。
Of course, a value could have more than those three properties and still belong to the type! The general rule is that values in an intersection type contain the union of properties in each of its constituents.
关于相交属性的直觉是正确的,但对于两个接口的联合,而不是它们的交集:
The intuition about intersecting properties is correct, but for the union of two interfaces, rather than their intersection:
typeK=keyof(Person|Lifespan);// Type is never
typeK=keyof(Person|Lifespan);// Type is never
没有 TypeScript 可以保证的键属于联合类型中的值,因此keyof联合必须是空集 ( never)。或者,更正式地说:
There are no keys that TypeScript can guarantee belong to a value in the union type, so keyof for the union must be the empty set (never). Or, more formally:
keyof(A&B)=(keyofA)|(keyofB)keyof(A|B)=(keyofA)&(keyofB)
keyof(A&B)=(keyofA)|(keyofB)keyof(A|B)=(keyofA)&(keyofB)
如果你能对为什么这些等式成立有直觉,你就会在理解 TypeScript 的类型系统方面取得很大进展!
If you can build an intuition for why these equations hold, you’ll have come a long way toward understanding TypeScript’s type system!
另一种可能更常见的编写PersonSpan类型的方法是extends:
Another perhaps more common way to write the PersonSpan type would be with extends:
interfacePerson{name:string;}interfacePersonSpanextendsPerson{birth:Date;death?:Date;}
interfacePerson{name:string;}interfacePersonSpanextendsPerson{birth:Date;death?:Date;}
思维类型作为值集,是什么extends意思?就像“可分配给”一样,您可以将其读作“子集”。中的每个值都PersonSpan必须有一个name属性,即string. 而且每个值还必须有一个birth属性,所以它是一个真子集。
Thinking of types as sets of values, what does extends mean? Just like “assignable to,” you can read it as “subset of.” Every value in PersonSpan must have a name property which is a string. And every value must also have a birth property, so it’s a proper subset.
你可能会听到“亚型”一词。这是另一种说法,一个集合的域是其他域的子集。从一维、二维和三维向量的角度思考:
You might hear the term “subtype.” This is another way of saying that one set’s domain is a subset of the others. Thinking in terms of one-, two-, and three-dimensional vectors:
interfaceVector1D{x:number;}interfaceVector2DextendsVector1D{y:number;}interfaceVector3DextendsVector2D{z:number;}
interfaceVector1D{x:number;}interfaceVector2DextendsVector1D{y:number;}interfaceVector3DextendsVector2D{z:number;}
你会说 aVector3D是 的子类型Vector2D,它是的子类型Vector1D(在类的上下文中你会说“子类”)。这种关系通常绘制为层次结构,但从值集的角度考虑,维恩图更合适(见图2-7)。
You’d say that a Vector3D is a subtype of Vector2D, which is a subtype of Vector1D (in the context of classes you’d say “subclass”). This relationship is usually drawn as a hierarchy, but thinking in terms of sets of values, a Venn diagram is more appropriate (see Figure 2-7).
使用维恩图,很明显,如果您重写接口时没有更改子集/子类型/可分配性关系extends:
With the Venn diagram, it’s clear that the subset/subtype/assignability relationships are unchanged if you rewrite the interfaces without extends:
interfaceVector1D{x:::::::number;}interfaceVector2D{x_number;y_number;}interfaceVector3D{x_number;y_number;z_number;}
interfaceVector1D{x:number;}interfaceVector2D{x:number;y:number;}interfaceVector3D{x:number;y:number;z:number;}
集合没有改变,所以维恩图也没有改变。
The sets haven’t changed, so neither has the Venn diagram.
虽然这两种解释都适用于对象类型,但当您开始考虑文字类型和联合类型时,集合解释会变得更加直观。extends也可以作为泛型类型中的约束出现,在这种情况下它也意味着“子集”(第 14 项):
While both interpretations are workable for object types, the set interpretation becomes much more intuitive when you start thinking about literal types and union types. extends can also appear as a constraint in a generic type, and it also means “subset of” in this context (Item 14):
functiongetKey<Kextendsstring>(val:any,key:K){// ...}
functiongetKey<Kextendsstring>(val:any,key:K){// ...}
延长是什么意思string?如果您习惯于从对象继承的角度来思考,那么它就很难解释了。您可以定义对象包装器类型的子类String(第 10 项),但这似乎不可取。
What does it mean to extend string? If you’re used to thinking in terms of object inheritance, it’s hard to interpret. You could define a subclass of the object wrapper type String (Item 10), but that seems inadvisable.
另一方面,从集合的角度考虑,它非常清楚:任何类型的域都是 的子集string。这包括字符串文字类型、字符串文字类型的联合及其string自身:
Thinking in terms of sets, on the other hand, it’s crystal clear: any type whose domain is a subset of string will do. This includes string literal types, unions of string literal types and string itself:
getKey({},'x');// OK, 'x' extends stringgetKey({},Math.random()<0.5?'a':'b');// OK, 'a'|'b' extends stringgetKey({},document.title);// OK, string extends stringgetKey({},12);// ~~ Type '12' is not assignable to parameter of type 'string'
getKey({},'x');// OK, 'x' extends stringgetKey({},Math.random()<0.5?'a':'b');// OK, 'a'|'b' extends stringgetKey({},document.title);// OK, string extends stringgetKey({},12);// ~~ Type '12' is not assignable to parameter of type 'string'
“extends”在上一个错误中变成了“assignable”,但这不应该让我们感到困惑,因为我们知道将两者都读作“subset of”。对于有限集,这也是一种有用的思维方式,例如您可能从 中获得的那些keyof T,它仅返回对象类型的键的类型:
“extends” has turned into “assignable” in the last error, but this shouldn’t trip us up since we know to read both as “subset of.” This is also a helpful mindset with finite sets, such the ones you might get from keyof T, which returns type for just the keys of an object type:
interfacePoint{x:number;y:number;}typePointKeys=keyofPoint;// Type is "x" | "y"functionsortBy<KextendskeyofT,T>(vals::::::::T[],key_K):T[]{// ...}constpts_Point[]=[{x_1,y_1},{x_2,y_0}];sortBy(pts,'x');// OK, 'x' extends 'x'|'y' (aka keyof T)sortBy(pts,'y');// OK, 'y' extends 'x'|'y'sortBy(pts,Math.random()<0.5?'x':'y');// OK, 'x'|'y' extends 'x'|'y'sortBy(pts,'z');// ~~~ Type '"z"' is not assignable to parameter of type '"x" | "y"
interfacePoint{x:number;y:number;}typePointKeys=keyofPoint;// Type is "x" | "y"functionsortBy<KextendskeyofT,T>(vals:T[],key:K):T[]{// ...}constpts:Point[]=[{x:1,y:1},{x:2,y:0}];sortBy(pts,'x');// OK, 'x' extends 'x'|'y' (aka keyof T)sortBy(pts,'y');// OK, 'y' extends 'x'|'y'sortBy(pts,Math.random()<0.5?'x':'y');// OK, 'x'|'y' extends 'x'|'y'sortBy(pts,'z');// ~~~ Type '"z"' is not assignable to parameter of type '"x" | "y"
当您的类型的关系不是严格分层时,集合解释也更有意义。例如,string|number和之间的关系是什么?string|Date它们的交集是非空的(它是string),但两者都不是另一个的子集。它们的域之间的关系很清楚,即使这些类型不符合严格的层次结构(见图2-8)。
The set interpretation also makes more sense when you have types whose relationship isn’t strictly hierarchical. What’s the relationship between string|number and string|Date, for instance? Their intersection is non-empty (it’s string), but neither is a subset of the other. The relationship between their domains is clear, even though these types don’t fit into a strict hierarchy (see Figure 2-8).
将类型视为集合也可以阐明数组和元组之间的关系。例如:
Thinking of types as sets can also clarify the relationships between arrays and tuples. For example:
constlist=[1,2];// Type is number[]consttuple:[number,number]=list;// ~~~~~ Type 'number[]' is missing the following// properties from type '[number, number]': 0, 1
constlist=[1,2];// Type is number[]consttuple:[number,number]=list;// ~~~~~ Type 'number[]' is missing the following// properties from type '[number, number]': 0, 1
是否有不是数字对的数字列表?当然!空列表和列表[1]是示例。number[]因此,不能分配给它是有道理的,[number, number]因为它不是它的子集。(反向分配确实有效。)
Are there lists of numbers which are not pairs of numbers? Sure! The empty list and the list [1] are examples. It therefore makes sense that number[] is not assignable to [number, number] since it’s not a subset of it. (The reverse assignment does work.)
三元组可以分配给一对吗?从结构类型的角度考虑,您可能希望如此。一对有0和1键,那么它是否也有其他键,比如2?
Is a triple assignable to a pair? Thinking in terms of structural typing, you might expect it to be. A pair has 0 and 1 keys, so mightn’t it have others, too, like 2?
consttriple:[number,number,number]=[1,2,3];constdouble:[number,number]=triple;// ~~~~~~ '[number, number, number]' is not assignable to '[number, number]'// Types of property 'length' are incompatible// Type '3' is not assignable to type '2'
consttriple:[number,number,number]=[1,2,3];constdouble:[number,number]=triple;// ~~~~~~ '[number, number, number]' is not assignable to '[number, number]'// Types of property 'length' are incompatible// Type '3' is not assignable to type '2'
答案是否定的,并且有一个有趣的原因。TypeScript不是将一对数字建模为{0: number, 1: number},而是将其建模为{0: number, 1: number, length: 2}。这是有道理的——你可以检查元组的长度——并且它排除了这个赋值。这可能是最好的!
The answer is “no,” and for an interesting reason. Rather than modeling a pair of numbers as {0: number, 1: number}, TypeScript models it as {0: number, 1: number, length: 2}. This makes sense—you can check the length of a tuple—and it precludes this assignment. And that’s probably for the best!
如果最好将类型视为值集,则意味着具有相同值集的两个类型是相同的。的确如此。除非两种类型在语义上不同并且恰好具有相同的域,否则没有理由两次定义相同的类型。
If types are best thought of as sets of values, that means that two types with the same sets of values are the same. And indeed this is true. Unless two types are semantically different and just happen to have the same domain, there’s no reason to define the same type twice.
最后,值得注意的是,并非所有值集都对应于 TypeScript 类型。x对于所有整数,或者所有具有和y属性但没有其他属性的对象,都没有 TypeScript 类型。有时您可以使用 减去类型Exclude,但前提是它会导致正确的 TypeScript 类型:
Finally, it’s worth noting that not all sets of values correspond to TypeScript types. There is no TypeScript type for all the integers, or for all the objects that have x and y properties but no others. You can sometimes subtract types using Exclude, but only when it would result in a proper TypeScript type:
typeT=Exclude<string|Date,string|number>;// Type is DatetypeNonZeroNums=Exclude<number,0>;// Type is still just number
typeT=Exclude<string|Date,string|number>;// Type is DatetypeNonZeroNums=Exclude<number,0>;// Type is still just number
表 2-1总结TypeScript 术语和集合论术语之间的对应关系。
Table 2-1 summarizes the correspondence between TypeScript terms and terms from set theory.
| TypeScript 术语 | 设定期限 |
|---|---|
|
∅(空集) ∅ (empty set) |
文字类型 Literal type |
单元素集 Single element set |
可分配给 T 的值 Value assignable to T |
值∈T(成员) Value ∈ T (member of) |
T1可分配给T2 T1 assignable to T2 |
T1 ⊆ T2(的子集) T1 ⊆ T2 (subset of) |
T1 扩展 T2 T1 extends T2 |
T1 ⊆ T2(的子集) T1 ⊆ T2 (subset of) |
T1 | T2 T1 | T2 |
T1 ∪ T2(并集) T1 ∪ T2 (union) |
T1 & T2 T1 & T2 |
T1∩T2(路口) T1 ∩ T2 (intersection) |
|
通用套装 Universal set |
将类型视为值集(类型的域)。这些集合可以是有限的(例如,boolean或文字类型)或无限的(例如,number或string)。
Think of types as sets of values (the type’s domain). These sets can either be finite (e.g., boolean or literal types) or infinite (e.g., number or string).
TypeScript 类型形成相交集(维恩图)而不是严格的层次结构。两种类型可以重叠而不是另一种的子类型。
TypeScript types form intersecting sets (a Venn diagram) rather than a strict hierarchy. Two types can overlap without either being a subtype of the other.
请记住,一个对象仍然可以属于一个类型,即使它具有类型声明中未提及的其他属性。
Remember that an object can still belong to a type even if it has additional properties that were not mentioned in the type declaration.
类型操作适用于集合的域。A和的交集是的域和的域B的交集。对于对象类型,这意味着 中的值同时具有和的属性。ABA & BAB
Type operations apply to a set’s domain. The intersection of A and B is the intersection of A’s domain and B’s domain. For object types, this means that values in A & B have the properties of both A and B.
Think of “extends,” “assignable to,” and “subtype of” as synonyms for “subset of.”
A symbol in TypeScript exists in one of two spaces:
类型空间
Type space
价值空间
Value space
这可能会让人感到困惑,因为同一个名字可以指代不同的东西,这取决于它所在的空间:
This can get confusing because the same name can refer to different things depending on which space it’s in:
interfaceCylinder{radius:number;height:number;}constCylinder=(radius:number,height:number)=>({radius,height});
interfaceCylinder{radius:number;height:number;}constCylinder=(radius:number,height:number)=>({radius,height});
interface Cylinder在类型空间中引入一个符号。const Cylinder在值空间中引入同名符号。他们彼此无关。根据上下文,当您键入 时Cylinder,您指的是类型或值。有时这会导致错误:
interface Cylinder introduces a symbol in type space. const Cylinder introduces a symbol with the same name in value space. They have nothing to do with one another. Depending on the context, when you type Cylinder, you’ll either be referring to the type or the value. Sometimes this can lead to errors:
functioncalculateVolume(shape:unknown){if(shapeinstanceofCylinder){shape.radius// ~~~~~~ Property 'radius' does not exist on type '{}'}}
functioncalculateVolume(shape:unknown){if(shapeinstanceofCylinder){shape.radius// ~~~~~~ Property 'radius' does not exist on type '{}'}}
这里发生了什么?您可能打算instanceof检查形状是否属于该Cylinder类型。但instanceof它是 JavaScript 的运行时运算符,它对值进行操作。所以instanceof Cylinder指的是功能,而不是类型。
What’s going on here? You probably intended the instanceof to check whether the shape was of the Cylinder type. But instanceof is JavaScript’s runtime operator, and it operates on values. So instanceof Cylinder refers to the function, not the type.
乍一看,符号是在类型空间还是值空间中并不总是很明显。您必须从符号出现的上下文中分辨出来。这可能会变得特别混乱,因为许多类型空间构造看起来与值空间构造完全相同。
It’s not always obvious at first glance whether a symbol is in type space or value space. You have to tell from the context in which the symbol occurs. This can get especially confusing because many type-space constructs look exactly the same as value-space constructs.
文字,例如:
Literals, for example:
typeT1='string literal';typeT2=123;constv1='string literal';constv2=123;
typeT1='string literal';typeT2=123;constv1='string literal';constv2=123;
type通常or之后的符号interface在类型空间中,而constorlet声明中引入的符号是值。
Generally the symbols after a type or interface are in type space while those introduced in a const or let declaration are values.
一为这两个空间建立直觉的最佳方法之一是通过TypeScript Playground,它向您展示了为您的 TypeScript 源代码生成的 JavaScript。类型在编译期间被擦除(第 3 项),因此如果一个符号消失,那么它可能在类型空间中(参见图 2-9)。
One of the best ways to build an intuition for the two spaces is through the TypeScript Playground, which shows you the generated JavaScript for your TypeScript source. Types are erased during compilation (Item 3), so if a symbol disappears then it was probably in type space (see Figure 2-9).
TypeScript 中的语句可以在类型空间和值空间之间交替。:类型声明 ( ) 或断言 ( )之后的符号as在类型空间中,而 an 之后的所有内容都=在值空间中。例如:
Statements in TypeScript can alternate between type space and value space. The symbols after a type declaration (:) or an assertion (as) are in type space while everything after an = is in value space. For example:
interfacePerson{first:string;last:string;}constp:Person={first:'Jane',last:'Jacobs'};// - --------------------------------- Values// ------ Type
interfacePerson{first:string;last:string;}constp:Person={first:'Jane',last:'Jacobs'};// - --------------------------------- Values// ------ Type
特别是函数语句可以在空格之间重复交替:
Function statements in particular can alternate repeatedly between the spaces:
function(p::::Person,subject_string,body_string)_Response{// ----- - ------- ---- Values// ------ ------ ------ -------- Types// ...}
function(p:Person,subject:string,body:string):Response{// ----- - ------- ---- Values// ------ ------ ------ -------- Types// ...}
这 class和enum构造同时引入类型和值。在第一个例子中,Cylinder应该是class:
The class and enum constructs introduce both a type and a value. In the first example, Cylinder should have been a class:
classCylinder{radius=1;height=1;}functioncalculateVolume(shape:unknown){if(shapeinstanceofCylinder){shape// OK, type is Cylindershape.radius// OK, type is number}}
classCylinder{radius=1;height=1;}functioncalculateVolume(shape:unknown){if(shapeinstanceofCylinder){shape// OK, type is Cylindershape.radius// OK, type is number}}
类引入的 TypeScript 类型基于其形状(其属性和方法),而值是构造函数。
The TypeScript type introduced by a class is based on its shape (its properties and methods) while the value is the constructor.
那里有许多运算符和关键字在类型或值上下文中表示不同的事物。typeof, 例如:
There are many operators and keywords that mean different things in a type or value context. typeof, for instance:
typeT1=typeofp;// Type is PersontypeT2=typeof;// Type is (p: Person, subject: string, body: string) => Responseconstv1=typeofp;// Value is "object"constv2=typeof;// Value is "function"
typeT1=typeofp;// Type is PersontypeT2=typeof;// Type is (p: Person, subject: string, body: string) => Responseconstv1=typeofp;// Value is "object"constv2=typeof;// Value is "function"
在类型上下文中,typeof获取一个值并返回其 TypeScript 类型。您可以将它们用作更大类型表达式的一部分,或使用type语句为它们命名。
In a type context, typeof takes a value and returns its TypeScript type. You can use these as part of a larger type expression, or use a type statement to give them a name.
在值上下文中,typeof是 JavaScript 的运行时运typeof算符。它返回一个包含符号运行时类型的字符串。这和 TypeScript 类型不一样!JavaScript 的运行时类型系统比 TypeScript 的静态类型系统简单得多。与无限多样的 TypeScript 类型相比,JavaScript 历史上只有六种运行时类型:“string”、“number”、“boolean”、“undefined”、“object”和“function”。
In a value context, typeof is JavaScript’s runtime typeof operator. It returns a string containing the runtime type of the symbol. This is not the same as the TypeScript type! JavaScript’s runtime type system is much simpler than TypeScript’s static type system. In contrast to the infinite variety of TypeScript types, there have historically only been six runtime types in JavaScript: “string,” “number,” “boolean,” “undefined,” “object,” and “function.”
typeof始终对值进行操作。您不能将其应用于类型。关键字class既引入了值又引入了类型,那么什么是typeof类呢?这取决于上下文:
typeof always operates on values. You can’t apply it to types. The class keyword introduces both a value and a type, so what is the typeof a class? It depends on the context:
constv=typeofCylinder;// Value is "function"typeT=typeofCylinder;// Type is typeof Cylinder
constv=typeofCylinder;// Value is "function"typeT=typeofCylinder;// Type is typeof Cylinder
价值"function"在于类在 JavaScript 中的实现方式。这种类型并不是特别有启发性。重要的是它不是 Cylinder(实例的类型)。它实际上是构造函数,您可以通过将其与以下内容一起使用来查看new:
The value is "function" because of how classes are implemented in JavaScript. The type isn’t particularly illuminating. What’s important is that it’s not Cylinder (the type of an instance). It’s actually the constructor function, which you can see by using it with new:
declareletfn:T;constc=newfn();// Type is Cylinder
declareletfn:T;constc=newfn();// Type is Cylinder
您可以使用泛型在构造函数类型和实例类型之间切换InstanceType:
You can go between the constructor type and the instance type using the InstanceType generic:
typeC=InstanceType<typeofCylinder>;// Type is Cylinder
typeC=InstanceType<typeofCylinder>;// Type is Cylinder
这 []属性访问器在类型空间中也有一个外观相同的等价物。但请注意,虽然obj['field']和obj.field在值空间中是等价的,但它们在类型空间中并不等价。您必须使用前者来获取另一个类型的属性的类型:
The [] property accessor also has an identical-looking equivalent in type space. But be aware that while obj['field'] and obj.field are equivalent in value space, they are not in type space. You must use the former to get the type of another type’s property:
constfirst:Person['first']=p['first'];// Or p.first// ----- ---------- Values// ------ ------- Types
constfirst:Person['first']=p['first'];// Or p.first// ----- ---------- Values// ------ ------- Types
Person['first']在这里是一种类型,因为它出现在类型上下文中(在 a 之后:)。您可以将任何类型放入索引槽中,包括联合类型或原始类型:
Person['first'] is a type here since it appears in a type context (after a :). You can put any type in the index slot, including union types or primitive types:
typePersonEl=Person['first'|'last'];// Type is stringtypeTuple=[string,number,Date];typeTupleEl=Tuple[number];// Type is string | number | Date
typePersonEl=Person['first'|'last'];// Type is stringtypeTuple=[string,number,Date];typeTupleEl=Tuple[number];// Type is string | number | Date
有关此的更多信息,请参见第 14 项。
See Item 14 for more on this.
还有许多其他结构在这两个空间中具有不同的含义:
There are many other constructs that have different meanings in the two spaces:
this在值空间是 JavaScript 的this关键字(条目 49)。作为一种类型,this是 的 TypeScript 类型this,又名“多态 this”。它有助于实现带有子类的方法链。
this in value space is JavaScript’s this keyword (Item 49). As a type, this is the TypeScript type of this, aka “polymorphic this.” It’s helpful for implementing method chains with subclasses.
In value space & and | are bitwise AND and OR. In type space they are the intersection and union operators.
const介绍一个新变量,但as const会更改文字或文字表达式的推断类型(条目 21)。
const introduces a new variable, but as const changes the inferred type of a literal or literal expression (Item 21).
extends能定义子类 ( class A extends B) 或子类型 ( interface A extends B) 或对泛型类型 ( ) 的约束Generic<T extends number>。
extends can define a subclass (class A extends B) or a subtype (interface A extends B) or a constraint on a generic type (Generic<T extends number>).
in能要么是循环 ( for (key in object)) 的一部分,要么是映射类型 ( Item 14 )。
in can either be part of a loop (for (key in object)) or a mapped type (Item 14).
如果 TypeScript 似乎根本无法理解您的代码,可能是因为类型和值空间的混淆。例如,假设您将email之前的函数更改为在单个对象参数中获取其参数:
If TypeScript doesn’t seem to understand your code at all, it may be because of confusion around type and value space. For example, say you change the email function from earlier to take its arguments in a single object parameter:
function(options:{person:Person,subject:string,body:string}){// ...}
function(options:{person:Person,subject:string,body:string}){// ...}
在 JavaScript 中,您可以使用解构赋值为对象中的每个属性创建局部变量:
In JavaScript you can use destructuring assignment to create local variables for each property in the object:
function({person,subject,body}){// ...}
function({person,subject,body}){// ...}
如果你尝试在 TypeScript 中做同样的事情,你会得到一些令人困惑的错误:
If you try to do the same in TypeScript, you get some confusing errors:
function({person:Person,// ~~~~~~ Binding element 'Person' implicitly has an 'any' typesubject:string,// ~~~~~~ Duplicate identifier 'string'// Binding element 'string' implicitly has an 'any' typebody:string}// ~~~~~~ Duplicate identifier 'string'// Binding element 'string' implicitly has an 'any' type){/* ... */}
function({person:Person,// ~~~~~~ Binding element 'Person' implicitly has an 'any' typesubject:string,// ~~~~~~ Duplicate identifier 'string'// Binding element 'string' implicitly has an 'any' typebody:string}// ~~~~~~ Duplicate identifier 'string'// Binding element 'string' implicitly has an 'any' type){/* ... */}
问题在于Person,并且string正在价值背景下进行解释。您正在尝试创建一个名为 的变量Person和两个名为 的变量string。相反,您应该将类型和值分开:
The problem is that Person and string are being interpreted in a value context. You’re trying to create a variable named Person and two variables named string. Instead, you should separate the types and values:
function({person,subject,body}:{person:Person,subject:string,body:string}){// ...}
function({person,subject,body}:{person:Person,subject:string,body:string}){// ...}
这明显更冗长,但在实践中,您可能有参数的命名类型,或者能够从上下文中推断它们(条目 26)。
This is significantly more verbose, but in practice you may have a named type for the parameters or be able to infer them from context (Item 26).
虽然类型和值的相似构造一开始可能会令人困惑,但一旦您掌握了它们,它们最终会用作助记符。
While the similar constructs in type and value can be confusing at first, they’re eventually useful as a mnemonic once you get the hang of it.
了解如何在阅读 TypeScript 表达式时判断您是在类型空间还是在值空间。使用 TypeScript 游乐场为此建立直觉。
Know how to tell whether you’re in type space or value space while reading a TypeScript expression. Use the TypeScript playground to build an intuition for this.
Every value has a type, but types do not have values. Constructs such as type and interface exist only in the type space.
"foo"可能是字符串文字,也可能是字符串文字类型。请注意这种区别并了解如何辨别它是什么。
"foo" might be a string literal, or it might be a string literal type. Be aware of this distinction and understand how to tell which it is.
typeof, this, 以及许多其他运算符和关键字在类型空间和值空间中具有不同的含义。
typeof, this, and many other operators and keywords have different meanings in type space and value space.
Some constructs such as class or enum introduce both a type and a value.
TypeScript 似乎有两种方法可以为变量赋值并赋予它类型:
TypeScript seems to have two ways of assigning a value to a variable and giving it a type:
interfacePerson{name:string};constalice:Person={name:'Alice'};// Type is Personconstbob={name:'Bob'}asPerson;// Type is Person
interfacePerson{name:string};constalice:Person={name:'Alice'};// Type is Personconstbob={name:'Bob'}asPerson;// Type is Person
虽然它们达到了相似的目的,但它们实际上是完全不同的!第一个 ( alice: Person) 为变量添加类型声明并确保值符合类型。后者 ( as Person) 执行类型断言。这告诉 TypeScript,尽管它推断出的类型是这样的,但您更了解并且希望类型为Person.
While these achieve similar ends, they are actually quite different! The first (alice: Person) adds a type declaration to the variable and ensures that the value conforms to the type. The latter (as Person) performs a type assertion. This tells TypeScript that, despite the type it inferred, you know better and would like the type to be Person.
通常,您应该更喜欢类型声明而不是类型断言。原因如下:
In general, you should prefer type declarations to type assertions. Here’s why:
constalice:Person={};// ~~~~~ Property 'name' is missing in type '{}'// but required in type 'Person'constbob={}asPerson;// No error
constalice:Person={};// ~~~~~ Property 'name' is missing in type '{}'// but required in type 'Person'constbob={}asPerson;// No error
类型声明验证值是否符合接口。因为它没有,TypeScript 会标记错误。类型断言通过告诉类型检查器,无论出于何种原因,你比它知道的更多,从而消除了这个错误。
The type declaration verifies that the value conforms to the interface. Since it does not, TypeScript flags an error. The type assertion silences this error by telling the type checker that, for whatever reason, you know better than it does.
如果您指定一个额外的属性,同样的事情会发生:
The same thing happens if you specify an additional property:
constalice:Person={name:'Alice',occupation:'TypeScript developer'// ~~~~~~~~~ Object literal may only specify known properties// and 'occupation' does not exist in type 'Person'};constbob={name:'Bob',occupation:'JavaScript developer'}asPerson;// No error
constalice:Person={name:'Alice',occupation:'TypeScript developer'// ~~~~~~~~~ Object literal may only specify known properties// and 'occupation' does not exist in type 'Person'};constbob={name:'Bob',occupation:'JavaScript developer'}asPerson;// No error
这是工作中的过度属性检查(第 11 项),但如果您使用断言则它不适用。
This is excess property checking at work (Item 11), but it doesn’t apply if you use an assertion.
因为它们提供额外的安全检查,所以您应该使用类型声明,除非您有特定的理由使用类型断言。
Because they provide additional safety checks, you should use type declarations unless you have a specific reason to use a type assertion.
你也可能会看到类似 的代码const bob = <Person>{}。这是断言的原始语法,相当于{} as Person. 它现在不太常见,因为在.tsx<Person>文件(TypeScript + React)中被解释为开始标记。
You may also see code that looks like const bob = <Person>{}. This was the original syntax for assertions and is equivalent to {} as Person. It is less common now because <Person> is interpreted as a start tag in .tsx files (TypeScript + React).
它是并不总是清楚如何使用带有箭头函数的声明。例如,如果您想Person在此代码中使用命名接口怎么办?
It’s not always clear how to use a declaration with arrow functions. For example, what if you wanted to use the named Person interface in this code?
constpeople=['alice','bob','jan'].map(name=>({name}));// { name: string; }[]... but we want Person[]
constpeople=['alice','bob','jan'].map(name=>({name}));// { name: string; }[]... but we want Person[]
在这里使用类型断言很诱人,它似乎可以解决问题:
It’s tempting to use a type assertion here, and it seems to solve the problem:
constpeople=['alice','bob','jan'].map(name=>({name}asPerson));// Type is Person[]
constpeople=['alice','bob','jan'].map(name=>({name}asPerson));// Type is Person[]
但这会遇到与更直接使用类型断言相同的问题。例如:
But this suffers from all the same issues as a more direct use of type assertions. For example:
constpeople=['alice','bob','jan'].map(name=>({}asPerson));// No error
constpeople=['alice','bob','jan'].map(name=>({}asPerson));// No error
那么如何在这种情况下使用类型声明呢?最直接的方法是在箭头函数中声明一个变量:
So how do you use a type declaration in this context instead? The most straightforward way is to declare a variable in the arrow function:
constpeople=['alice','bob','jan'].map(name=>{constperson:Person={name};returnperson});// Type is Person[]
constpeople=['alice','bob','jan'].map(name=>{constperson:Person={name};returnperson});// Type is Person[]
但是与原始代码相比,这引入了相当大的噪音。更简洁的方法是声明箭头函数的返回类型:
But this introduces considerable noise compared to the original code. A more concise way is to declare the return type of the arrow function:
constpeople=['alice','bob','jan'].map((name):Person=>({name}));// Type is Person[]
constpeople=['alice','bob','jan'].map((name):Person=>({name}));// Type is Person[]
这将对值执行与先前版本相同的所有检查。这里的括号很重要!(name): Person推断的类型name并指定返回的类型应为Person。但是会指定as(name: Person)的类型并允许推断返回类型,这会产生错误。namePerson
This performs all the same checks on the value as the previous version. The parentheses are significant here! (name): Person infers the type of name and specifies that the returned type should be Person. But (name: Person) would specify the type of name as Person and allow the return type to be inferred, which would produce an error.
在这种情况下,您还可以编写最终所需的类型并让 TypeScript 检查赋值的有效性:
In this case you could have also written the final desired type and let TypeScript check the validity of the assignment:
constpeople:Person[]=['alice','bob','jan'].map((name):Person=>({name}));
constpeople:Person[]=['alice','bob','jan'].map((name):Person=>({name}));
但是在较长的函数调用链的上下文中,可能有必要或希望更早地使用命名类型。它将有助于在错误发生的地方标记错误。
But in the context of a longer chain of function calls it may be necessary or desirable to have the named type in place earlier. And it will help flag errors where they occur.
那么什么时候应该使用类型断言呢?当您确实比 TypeScript 更了解一个类型时,类型断言最有意义,通常来自类型检查器无法获得的上下文。例如,您可能比 TypeScript 更准确地知道 DOM 元素的类型:
So when should you use a type assertion? Type assertions make the most sense when you truly do know more about a type than TypeScript does, typically from context that isn’t available to the type checker. For instance, you may know the type of a DOM element more precisely than TypeScript does:
document.querySelector('#myButton').addEventListener('click',e=>{e.currentTarget// Type is EventTargetconstbutton=e.currentTargetasHTMLButtonElement;button// Type is HTMLButtonElement});
document.querySelector('#myButton').addEventListener('click',e=>{e.currentTarget// Type is EventTargetconstbutton=e.currentTargetasHTMLButtonElement;button// Type is HTMLButtonElement});
因为 TypeScript 无法访问页面的 DOM,所以它无法知道这#myButton是一个按钮元素。而且它不知道currentTarget事件的事件应该是同一个按钮。由于您拥有 TypeScript 没有的信息,因此类型断言在这里很有意义。有关 DOM 类型的更多信息,请参阅条目 55。
Because TypeScript doesn’t have access to the DOM of your page, it has no way of knowing that #myButton is a button element. And it doesn’t know that the currentTarget of the event should be that same button. Since you have information that TypeScript does not, a type assertion makes sense here. For more on DOM types, see Item 55.
您可能还会遇到非空断言,这种断言非常常见,以至于它有一个特殊的语法:
You may also run into the non-null assertion, which is so common that it gets a special syntax:
constelNull=document.getElementById('foo');// Type is HTMLElement | nullconstel=document.getElementById('foo')!;// Type is HTMLElement
constelNull=document.getElementById('foo');// Type is HTMLElement | nullconstel=document.getElementById('foo')!;// Type is HTMLElement
用过的作为前缀,!是布尔否定。但作为后缀,!被解释为该值不为空的断言。你应该!像对待任何其他断言一样对待它:它在编译期间被删除,所以你应该只在你有类型检查器缺少的信息并且可以确保该值不为空时使用它。如果不能,则应使用条件来检查大小写null。
Used as a prefix, ! is boolean negation. But as a suffix, ! is interpreted as an assertion that the value is non-null. You should treat ! just like any other assertion: it is erased during compilation, so you should only use it if you have information that the type checker lacks and can ensure that the value is non-null. If you can’t, you should use a conditional to check for the null case.
类型断言有其局限性:它们不允许您在任意类型之间进行转换。一般的想法是,如果其中一个是另一个的子集,则可以使用类型断言在 A 和 B 之间进行转换。HTMLElement是 的子类型HTMLElement | null,所以这个类型断言是可以的。HTMLButtonElement是 的子类型EventTarget,所以也可以。AndPerson是 的子类型{},因此该断言也没有问题。
Type assertions have their limits: they don’t let you convert between arbitrary types. The general idea is that you can use a type assertion to convert between A and B if either is a subset of the other. HTMLElement is a subtype of HTMLElement | null, so this type assertion is OK. HTMLButtonElement is a subtype of EventTarget, so that was OK, too. And Person is a subtype of {}, so that assertion is also fine.
但是你不能在 aPerson和 an之间转换HTMLElement,因为两者都不是另一个的子类型:
But you can’t convert between a Person and an HTMLElement since neither is a subtype of the other:
interfacePerson{name:string;}constbody=document.body;constel=bodyasPerson;// ~~~~~~~~~~~~~~ Conversion of type 'HTMLElement' to type 'Person'// may be a mistake because neither type sufficiently// overlaps with the other. If this was intentional,// convert the expression to 'unknown' first
interfacePerson{name:string;}constbody=document.body;constel=bodyasPerson;// ~~~~~~~~~~~~~~ Conversion of type 'HTMLElement' to type 'Person'// may be a mistake because neither type sufficiently// overlaps with the other. If this was intentional,// convert the expression to 'unknown' first
该错误暗示了逃生舱口,即使用类型unknown(Item 42)。每种类型都是 的子类型unknown,因此涉及的断言unknown总是可以的。这使您可以在任意类型之间进行转换,但至少您明确表示您在做一些可疑的事情!
The error suggests an escape hatch, namely, using the unknown type (Item 42). Every type is a subtype of unknown, so assertions involving unknown are always OK. This lets you convert between arbitrary types, but at least you’re being explicit that you’re doing something suspicious!
constel=document.bodyasunknownasPerson;// OK
constel=document.bodyasunknownasPerson;// OK
在除了对象之外,JavaScript 还有七种原始值:字符串、数字、布尔值、null、undefined、符号和 bigint。前五个从一开始就存在。这ES2015 中添加了 symbol primitive,bigint 正在完善中。
In addition to objects, JavaScript has seven types of primitive values: strings, numbers, booleans, null, undefined, symbol, and bigint. The first five have been around since the beginning. The symbol primitive was added in ES2015, and bigint is in the process of being finalized.
基元与对象的区别在于不可变且没有方法。你可能会反对字符串确实有方法:
Primitives are distinguished from objects by being immutable and not having methods. You might object that strings do have methods:
> '原始'.charAt(3) "m"
> 'primitive'.charAt(3) "m"
但事情并不像他们看起来的那样。这里实际上发生了一些令人惊讶和微妙的事情。虽然字符串原语没有方法,但 JavaScript 还定义了一个String 对象类型。JavaScript 在这些类型之间自由转换。charAt当你像在字符串基元上访问一个方法时,JavaScript 将它包装在一个String对象中,调用该方法,然后丢弃该对象。
But things are not quite as they seem. There’s actually something surprising and subtle going on here. While a string primitive does not have methods, JavaScript also defines a String object type that does. JavaScript freely converts between these types. When you access a method like charAt on a string primitive, JavaScript wraps it in a String object, calls the method, and then throws the object away.
如果你使用 monkey-patch String.prototype(项目 43),你可以观察到这一点:
You can observe this if you monkey-patch String.prototype (Item 43):
// Don't do this!constoriginalCharAt=String.prototype.charAt;String.prototype.charAt=function(pos){console.log(this,typeofthis,pos);returnoriginalCharAt.call(this,pos);};console.log('primitive'.charAt(3));
// Don't do this!constoriginalCharAt=String.prototype.charAt;String.prototype.charAt=function(pos){console.log(this,typeofthis,pos);returnoriginalCharAt.call(this,pos);};console.log('primitive'.charAt(3));
这会产生以下输出:
This produces the following output:
[字符串:'原始'] '对象' 3 米
[String: 'primitive'] 'object' 3 m
this方法中的值是对象String包装器,而不是字符串基元。您可以String直接实例化一个对象,它有时会表现得像一个字符串基元。但不总是。例如,一个String对象永远只等于它自己:
The this value in the method is a String object wrapper, not a string primitive. You can instantiate a String object directly and it will sometimes behave like a string primitive. But not always. For example, a String object is only ever equal to itself:
> “你好” === 新字符串(“你好”) 错误的 >新字符串(“你好”)=== 新字符串(“你好”) 假
> "hello" === new String("hello")
false
> new String("hello") === new String("hello")
false
对象包装器类型的隐式转换解释了 JavaScript 中的一个奇怪现象——如果你将一个属性分配给一个基本类型,它就会消失:
The implicit conversion to object wrapper types explains an odd phenomenon in JavaScript—if you assign a property to a primitive, it disappears:
> x = "你好" > x.language = '英语' '英语' > x.language 未定义
> x = "hello" > x.language = 'English' 'English' > x.language undefined
现在你知道了解释:x被转换为一个String实例,language属性被设置在它上面,然后对象(及其language属性)被丢弃。
Now you know the explanation: x is converted to a String instance, the language property is set on that, and then the object (with its language property) is thrown away.
其他基元也有对象包装器类型:Number数字、Boolean布尔值、Symbol符号和BigIntbigints(没有对象包装器null和undefined)。
There are object wrapper types for the other primitives as well: Number for numbers, Boolean for booleans, Symbol for symbols, and BigInt for bigints (there are no object wrappers for null and undefined).
这些包装器类型的存在是为了方便在原始值上提供方法和提供静态方法(例如,String.fromCharCode)。但通常没有理由直接实例化它们。
These wrapper types exist as a convenience to provide methods on the primitive values and to provide static methods (e.g., String.fromCharCode). But there’s usually no reason to instantiate them directly.
TypeScript 通过为基元及其对象包装器设置不同的类型来模拟这种区别:
TypeScript models this distinction by having distinct types for the primitives and their object wrappers:
string和String
string and String
number和Number
number and Number
boolean和Boolean
boolean and Boolean
symbol和Symbol
symbol and Symbol
bigint和BigInt
bigint and BigInt
它是容易无意中键入String(尤其是如果您来自 Java 或 C#)并且它甚至似乎可以工作,至少在最初是这样:
It’s easy to inadvertently type String (especially if you’re coming from Java or C#) and it even seems to work, at least initially:
functiongetStringLen(foo:String){returnfoo.length;}getStringLen("hello");// OKgetStringLen(newString("hello"));// OK
functiongetStringLen(foo:String){returnfoo.length;}getStringLen("hello");// OKgetStringLen(newString("hello"));// OK
但是当您尝试将String对象传递给需要 a 的方法时,事情就出错了string:
But things go awry when you try to pass a String object to a method that expects a string:
functionisGreeting(phrase:String){return['hello','good day'].includes(phrase);// ~~~~~~// Argument of type 'String' is not assignable to parameter// of type 'string'.// 'string' is a primitive, but 'String' is a wrapper object;// prefer using 'string' when possible}
functionisGreeting(phrase:String){return['hello','good day'].includes(phrase);// ~~~~~~// Argument of type 'String' is not assignable to parameter// of type 'string'.// 'string' is a primitive, but 'String' is a wrapper object;// prefer using 'string' when possible}
Sostring可分配给String,但String不可分配给string。令人困惑?遵循错误消息中的建议并坚持使用string. TypeScript 附带的所有类型声明都使用它,几乎所有其他库的类型也是如此。
So string is assignable to String, but String is not assignable to string. Confusing? Follow the advice in the error message and stick with string. All the type declarations that ship with TypeScript use it, as do the typings for almost all other libraries.
另一种可以使用包装器对象的方法是,如果您提供带有大写字母的显式类型注释:
Another way you can wind up with wrapper objects is if you provide an explicit type annotation with a capital letter:
consts:String="primitive";constn:Number=12;constb:Boolean=true;
consts:String="primitive";constn:Number=12;constb:Boolean=true;
当然,运行时的值仍然是原始值,而不是对象。但是 TypeScript 允许这些声明,因为原始类型可以分配给对象包装器。这些注释既具有误导性又是多余的(条目 19)。最好坚持使用原始类型。
Of course, the values at runtime are still primitives, not objects. But TypeScript permits these declarations because the primitive types are assignable to the object wrappers. These annotations are both misleading and redundant (Item 19). Better to stick with the primitive types.
最后一点,可以调用BigIntand Symbolwithout new,因为它们会创建原语:
As a final note, it’s OK to call BigInt and Symbol without new, since these create primitives:
> BigInt 类型(1234)
“bigint”
> typeof Symbol('sym')
“符号”> typeof BigInt(1234)
"bigint"
> typeof Symbol('sym')
"symbol"
这些是BigInt和Symbol 值,而不是 TypeScript 类型(第 8 项)。bigint调用它们会产生类型和的值symbol。
These are the BigInt and Symbol values, not the TypeScript types (Item 8). Calling them results in values of type bigint and symbol.
了解对象包装器类型如何用于提供有关原始值的方法。避免实例化它们或直接使用它们。
Understand how object wrapper types are used to provide methods on primitive values. Avoid instantiating them or using them directly.
避免使用 TypeScript 对象包装器类型。改用原始类型:string代替String,number代替Number,boolean代替Boolean,symbol代替Symbol,bigint代替BigInt.
Avoid TypeScript object wrapper types. Use the primitive types instead: string instead of String, number instead of Number, boolean instead of Boolean, symbol instead of Symbol, and bigint instead of BigInt.
什么时候您将对象文字分配给具有声明类型的变量,TypeScript 确保它具有该类型的属性而没有其他类型:
When you assign an object literal to a variable with a declared type, TypeScript makes sure it has the properties of that type and no others:
interfaceRoom{numDoors::::::number;ceilingHeightFt_number;}constr_Room={numDoors_1,ceilingHeightFt_10,elephant:'present',// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known properties,// and 'elephant' does not exist in type 'Room'};
interfaceRoom{numDoors:number;ceilingHeightFt:number;}constr:Room={numDoors:1,ceilingHeightFt:10,elephant:'present',// ~~~~~~~~~~~~~~~~~~ Object literal may only specify known properties,// and 'elephant' does not exist in type 'Room'};
虽然有一个elephant属性很奇怪,但从结构类型的角度来看,这个错误没有多大意义(第 4 项)。该常量可分配给Room类型,您可以通过引入一个中间变量来查看:
While it is odd that there’s an elephant property, this error doesn’t make much sense from a structural typing point of view (Item 4). That constant is assignable to the Room type, which you can see by introducing an intermediate variable:
constobj={numDoors:1,ceilingHeightFt:10,elephant:'present',};constr:Room=obj;// OK
constobj={numDoors:1,ceilingHeightFt:10,elephant:'present',};constr:Room=obj;// OK
的类型obj被推断为{ numDoors: number; ceilingHeightFt: number; elephant: string }。因为此类型包含该类型中值的子集Room,所以它可分配给Room,并且代码通过了类型检查器(请参阅第 7 项)。
The type of obj is inferred as { numDoors: number; ceilingHeightFt: number; elephant: string }. Because this type includes a subset of the values in the Room type, it is assignable to Room, and the code passes the type checker (see Item 7).
那么这两个例子有什么不同呢?首先,您触发了一个称为“过多属性检查”的过程,该过程有助于捕获结构类型系统否则会遗漏的一类重要错误。但是这个过程有其局限性,将其与常规可分配性检查混为一谈会使建立结构类型直觉变得更加困难。将额外的属性检查识别为一个独特的过程,将帮助您构建一个更清晰的 TypeScript 类型系统心智模型。
So what is different about these two examples? In the first you’ve triggered a process known as “excess property checking,” which helps catch an important class of errors that the structural type system would otherwise miss. But this process has its limits, and conflating it with regular assignability checks can make it harder to build an intuition for structural typing. Recognizing excess property checking as a distinct process will help you build a clearer mental model of TypeScript’s type system.
正如Item 1所解释的,TypeScript 不仅仅试图标记将在运行时抛出异常的代码。它还会尝试查找不符合您预期的代码。这是后者的一个例子:
As Item 1 explained, TypeScript goes beyond trying to flag code that will throw exceptions at runtime. It also tries to find code that doesn’t do what you intend. Here’s an example of the latter:
interfaceOptions{title::::string;darkMode?_boolean;}functioncreateWindow(options_Options){if(options.darkMode){setDarkMode();}// ...}createWindow({title:'Spider Solitaire',darkmode_true// ~~~~~~~~~~~~~ Object literal may only specify known properties, but// 'darkmode' does not exist in type 'Options'.// Did you mean to write 'darkMode'?});
interfaceOptions{title:string;darkMode?:boolean;}functioncreateWindow(options:Options){if(options.darkMode){setDarkMode();}// ...}createWindow({title:'Spider Solitaire',darkmode:true// ~~~~~~~~~~~~~ Object literal may only specify known properties, but// 'darkmode' does not exist in type 'Options'.// Did you mean to write 'darkMode'?});
此代码在运行时不会引发任何类型的错误。但它也不太可能按照 TypeScript 所说的确切原因执行您的预期操作:它应该是darkMode(大写 M),而不是darkmode.
This code doesn’t throw any sort of error at runtime. But it’s also unlikely to do what you intended for the exact reason that TypeScript says: it should be darkMode (capital M), not darkmode.
A纯粹的结构类型检查器无法发现此类错误,因为类型的范围Options非常广泛:它包括所有具有titlea 属性string和任何其他属性的对象,只要这些对象不包含darkMode属性集true或以外的东西false。
A purely structural type checker wouldn’t be able to spot this sort of error because the domain of the Options type is incredibly broad: it includes all objects with a title property that’s a string and any other properties, so long as those don’t include a darkMode property set to something other than true or false.
人们很容易忘记 TypeScript 类型的扩展性。以下是可分配给的更多值Options:
It’s easy to forget how expansive TypeScript types can be. Here are a few more values that are assignable to Options:
consto1:Options=document;// OKconsto2:Options=newHTMLAnchorElement;// OK
consto1:Options=document;// OKconsto2:Options=newHTMLAnchorElement;// OK
和document的实例都HTMLAnchorElement具有title字符串属性,因此这些赋值是可以的。Options确实是一个广泛的类型!
Both document and instances of HTMLAnchorElement have title properties that are strings, so these assignments are OK. Options is a broad type indeed!
过多的属性检查试图在不损害类型系统的基本结构性质的情况下控制这种情况。它通过专门禁止对象文字上的未知属性来实现这一点。(出于这个原因,它有时被称为“严格的对象字面量检查”。)对象字面量document也不是new HTMLAnchorElement,所以它们没有触发检查。但{title, darkmode}对象是,所以它确实是:
Excess property checking tries to rein this in without compromising the fundamentally structural nature of the type system. It does this by disallowing unknown properties specifically on object literals. (It’s sometimes called “strict object literal checking” for this reason.) Neither document nor new HTMLAnchorElement is an object literal, so they did not trigger the checks. But the {title, darkmode} object is, so it does:
consto:Options={darkmode:true,title:'Ski Free'};// ~~~~~~~~ 'darkmode' does not exist in type 'Options'...
consto:Options={darkmode:true,title:'Ski Free'};// ~~~~~~~~ 'darkmode' does not exist in type 'Options'...
这解释了为什么使用没有类型注释的中间变量会使错误消失:
This explains why using an intermediate variable without a type annotation makes the error go away:
constintermediate={darkmode:true,title:'Ski Free'};consto:Options=intermediate;// OK
constintermediate={darkmode:true,title:'Ski Free'};consto:Options=intermediate;// OK
虽然第一行的右侧是对象字面量,但第二行 ( ) 的右侧intermediate不是,因此多余的属性检查不适用,错误消失了。
While the righthand side of the first line is an object literal, the righthand side of the second line (intermediate) is not, so excess property checking does not apply, and the error goes away.
使用类型断言时不会发生过多的属性检查:
Excess property checking does not happen when you use a type assertion:
consto={darkmode:true,title:'Ski Free'}asOptions;// OK
consto={darkmode:true,title:'Ski Free'}asOptions;// OK
这是更喜欢声明而不是断言的一个很好的理由(第 9 项)。
This is a good reason to prefer declarations to assertions (Item 9).
如果你不想要这种检查,你可以使用索引签名告诉 TypeScript 期待额外的属性:
If you don’t want this sort of check, you can tell TypeScript to expect additional properties using an index signature:
interfaceOptions{darkMode?::::boolean;[otherOptions_string]:unknown;}consto_Options={darkmode_true};// OK
interfaceOptions{darkMode?:boolean;[otherOptions:string]:unknown;}consto:Options={darkmode:true};// OK
第 15 项讨论了何时适合和不适合为您的数据建模。
Item 15 discusses when this is and is not an appropriate way to model your data.
对“弱”类型进行相关检查,这些类型只有可选属性:
A related check happens for “weak” types, which have only optional properties:
interfaceLineChartOptions{logscale?::::::boolean;invertedYAxis?_boolean;areaChart?_boolean;}constopts={logScale_true};consto_LineChartOptions=opts;// ~ Type '{ logScale: boolean; }' has no properties in common// with type 'LineChartOptions'
interfaceLineChartOptions{logscale?:boolean;invertedYAxis?:boolean;areaChart?:boolean;}constopts={logScale:true};consto:LineChartOptions=opts;// ~ Type '{ logScale: boolean; }' has no properties in common// with type 'LineChartOptions'
从结构上看,LineChartOptions类型应该包括几乎所有的对象。对于像这样的弱类型,TypeScript 添加了另一项检查以确保值类型和声明的类型至少有一个共同的属性。就像过度的属性检查一样,这可以有效地捕捉拼写错误并且不是严格的结构性的。但与过度属性检查不同,它发生在涉及弱类型的所有可分配性检查期间。分解出中间变量不会绕过此检查。
From a structural point of view, the LineChartOptions type should include almost all objects. For weak types like this, TypeScript adds another check to make sure that the value type and declared type have at least one property in common. Much like excess property checking, this is effective at catching typos and isn’t strictly structural. But unlike excess property checking, it happens during all assignability checks involving weak types. Factoring out an intermediate variable doesn’t bypass this check.
额外的属性检查是捕获属性名称中的拼写错误和其他错误的有效方法,否则结构类型系统将允许这些错误。它对于Options包含可选字段的类型特别有用。但它的范围也非常有限:它只适用于对象字面量。认识到这一局限性并区分多余的属性检查和普通类型检查。这将帮助您建立两者的心智模型。
Excess property checking is an effective way of catching typos and other mistakes in property names that would otherwise be allowed by the structural typing system. It’s particularly useful with types like Options that contain optional fields. But it is also very limited in scope: it only applies to object literals. Recognize this limitation and distinguish between excess property checking and ordinary type checking. This will help you build a mental model of both.
分解出一个常量在这里消除了错误,但它也会在其他上下文中引入错误。有关这方面的示例,请参见第 26 项。
Factoring out a constant made an error go away here, but it can also introduce an error in other contexts. See Item 26 for examples of this.
当您将对象文字分配给变量或将其作为参数传递给函数时,它会进行额外的属性检查。
When you assign an object literal to a variable or pass it as an argument to a function, it undergoes excess property checking.
过多的属性检查是一种查找错误的有效方法,但它不同于 TypeScript 类型检查器执行的通常的结构可分配性检查。将这些过程混为一谈会让你更难建立可分配性的心智模型。
Excess property checking is an effective way to find errors, but it is distinct from the usual structural assignability checks done by the TypeScript type checker. Conflating these processes will make it harder for you to build a mental model of assignability.
Be aware of the limits of excess property checking: introducing an intermediate variable will remove these checks.
JavaScript(和 TypeScript)区分函数语句和函数表达式:
JavaScript (and TypeScript) distinguishes a function statement and a function expression:
functionrollDice1(sides:number):number{/* ... */}// StatementconstrollDice2=function(sides:number):number{/* ... */};// ExpressionconstrollDice3=(sides:number):number=>{/* ... */};// Also expression
functionrollDice1(sides:number):number{/* ... */}// StatementconstrollDice2=function(sides:number):number{/* ... */};// ExpressionconstrollDice3=(sides:number):number=>{/* ... */};// Also expression
TypeScript 中函数表达式的一个优点是您可以一次将类型声明应用于整个函数,而不是单独指定参数的类型和返回类型:
An advantage of function expressions in TypeScript is that you can apply a type declaration to the entire function at once, rather than specifying the types of the parameters and return type individually:
typeDiceRollFn=(sides:number)=>number;constrollDice:DiceRollFn=sides=>{/* ... */};
typeDiceRollFn=(sides:number)=>number;constrollDice:DiceRollFn=sides=>{/* ... */};
如果将鼠标悬停sides在编辑器中,您会看到 TypeScript 知道它的类型是number. 在这样一个简单的示例中,函数类型并没有提供太多价值,但该技术确实开辟了许多可能性。
If you mouse over sides in your editor, you’ll see that TypeScript knows its type is number. The function type doesn’t provide much value in such a simple example, but the technique does open up a number of possibilities.
一是减少重复。例如,如果你想写几个函数来对数字进行算术运算,你可以这样写:
One is reducing repetition. If you wanted to write several functions for doing arithmetic on numbers, for instance, you could write them like this:
functionadd(a:::::::::number,b_number){returna+b;}functionsub(a_number,b_number){returna-b;}functionmul(a_number,b_number){returna*b;}functiondiv(a_number,b_number){returna/b;}
functionadd(a:number,b:number){returna+b;}functionsub(a:number,b:number){returna-b;}functionmul(a:number,b:number){returna*b;}functiondiv(a:number,b:number){returna/b;}
或者将重复的函数签名合并为一个函数类型:
or consolidate the repeated function signatures with a single function type:
typeBinaryFn=(a:::::::number,b_number)=>number;constadd_BinaryFn=(a,b)=>a+b;constsub_BinaryFn=(a,b)=>a-b;constmul_BinaryFn=(a,b)=>a*b;constdiv_BinaryFn=(a,b)=>a/b;
typeBinaryFn=(a:number,b:number)=>number;constadd:BinaryFn=(a,b)=>a+b;constsub:BinaryFn=(a,b)=>a-b;constmul:BinaryFn=(a,b)=>a*b;constdiv:BinaryFn=(a,b)=>a/b;
这比以前有更少的类型注释,并且它们与函数实现分开。这使逻辑更加明显。您还检查了所有函数表达式的返回类型是否为number.
This has fewer type annotations than before, and they’re separated away from the function implementations. This makes the logic more apparent. You’ve also gained a check that the return type of all the function expressions is number.
库通常为通用函数签名提供类型。例如,ReactJS 提供了一种MouseEventHandler类型,您可以将其应用于整个函数,而不是指定MouseEvent为函数参数的类型。如果如果您是图书馆作者,请考虑为常见回调提供类型声明。
Libraries often provide types for common function signatures. For example, ReactJS provides a MouseEventHandler type that you can apply to an entire function rather than specifying MouseEvent as a type for the function’s parameter. If you’re a library author, consider providing type declarations for common callbacks.
其他您可能希望将类型应用于函数表达式的地方是匹配其他函数的签名。例如,在 Web 浏览器中,该fetch函数发出对某些资源的 HTTP 请求:
Another place you might want to apply a type to a function expression is to match the signature of some other function. In a web browser, for example, the fetch function issues an HTTP request for some resource:
constresponseP=fetch('/quote?by=Mark+Twain');// Type is Promise<Response>
constresponseP=fetch('/quote?by=Mark+Twain');// Type is Promise<Response>
response.json()您通过or从响应中提取数据response.text():
You extract data from the response via response.json() or response.text():
asyncfunctiongetQuote() {constresponse=awaitfetch('/quote?by=Mark+Twain');constquote=awaitresponse.json();returnquote;}// {// "quote": "If you tell the truth, you don't have to remember anything.",// "source": "notebook",// "date": "1894"// }
asyncfunctiongetQuote() {constresponse=awaitfetch('/quote?by=Mark+Twain');constquote=awaitresponse.json();returnquote;}// {// "quote": "If you tell the truth, you don't have to remember anything.",// "source": "notebook",// "date": "1894"// }
(有关 Promises 和/的更多信息,请参阅第 25 项。)asyncawait
(See Item 25 for more on Promises and async/await.)
这里有一个错误:如果请求/quote失败,响应正文很可能包含“404 Not Found”之类的解释。这不是 JSON,因此response.json()将返回一个被拒绝的 Promise,并带有一条关于无效 JSON 的消息。这掩盖了真正的错误,即 404。
There’s a bug here: if the request for /quote fails, the response body is likely to contain an explanation like “404 Not Found.” This isn’t JSON, so response.json() will return a rejected Promise with a message about invalid JSON. This obscures the real error, which was a 404.
很容易忘记错误响应fetch不会导致 Promise 被拒绝。让我们写一个checkedFetch函数来为我们做状态检查。fetchin的类型声明lib.dom.d.ts如下所示:
It’s easy to forget that an error response with fetch does not result in a rejected Promise. Let’s write a checkedFetch function to do the status check for us. The type declarations for fetch in lib.dom.d.ts look like this:
declarefunctionfetch(input:RequestInfo,init?:RequestInit):Promise<Response>;
declarefunctionfetch(input:RequestInfo,init?:RequestInit):Promise<Response>;
所以你可以checkedFetch这样写:
So you can write checkedFetch like this:
asyncfunctioncheckedFetch(input:RequestInfo,init?:RequestInit){constresponse=awaitfetch(input,init);if(!response.ok){// Converted to a rejected Promise in an async functionthrownewError('Request failed: '+response.status);}returnresponse;}
asyncfunctioncheckedFetch(input:RequestInfo,init?:RequestInit){constresponse=awaitfetch(input,init);if(!response.ok){// Converted to a rejected Promise in an async functionthrownewError('Request failed: '+response.status);}returnresponse;}
这行得通,但可以写得更简洁:
This works, but it can be written more concisely:
constcheckedFetch:typeoffetch=async(input,init)=>{constresponse=awaitfetch(input,init);if(!response.ok){thrownewError('Request failed: '+response.status);}returnresponse;}
constcheckedFetch:typeoffetch=async(input,init)=>{constresponse=awaitfetch(input,init);if(!response.ok){thrownewError('Request failed: '+response.status);}returnresponse;}
我们已经从函数语句更改为函数表达式,并将类型 ( typeof fetch) 应用于整个函数。input这允许 TypeScript 推断和参数的类型init。
We’ve changed from a function statement to a function expression and applied a type (typeof fetch) to the entire function. This allows TypeScript to infer the types of the input and init parameters.
类型注释还保证 的返回类型与checkedFetch的返回类型相同fetch。例如,如果您编写的return不是,TypeScript 就会发现错误:throw
The type annotation also guarantees that the return type of checkedFetch will be the same as that of fetch. Had you written return instead of throw, for example, TypeScript would have caught the mistake:
constcheckedFetch:typeoffetch=async(input,init)=>{// ~~~~~~~~~~~~ Type 'Promise<Response | HTTPError>'// is not assignable to type 'Promise<Response>'// Type 'Response | HTTPError' is not assignable// to type 'Response'constresponse=awaitfetch(input,init);if(!response.ok){returnnewError('Request failed: '+response.status);}returnresponse;}
constcheckedFetch:typeoffetch=async(input,init)=>{// ~~~~~~~~~~~~ Type 'Promise<Response | HTTPError>'// is not assignable to type 'Promise<Response>'// Type 'Response | HTTPError' is not assignable// to type 'Response'constresponse=awaitfetch(input,init);if(!response.ok){returnnewError('Request failed: '+response.status);}returnresponse;}
第一个示例中的相同错误可能会导致错误,但在调用 的代码中checkedFetch,而不是在实现中。
The same mistake in the first example would likely have led to an error, but in the code that called checkedFetch, rather than in the implementation.
除了更简洁之外,键入整个函数表达式而不是其参数还为您提供了更好的安全性。当您编写一个与另一个函数具有相同类型签名的函数,或编写多个具有相同类型签名的函数时,请考虑您是否可以将类型声明应用于整个函数,而不是重复参数和返回值的类型。
In addition to being more concise, typing this entire function expression instead of its parameters has given you better safety. When you’re writing a function that has the same type signature as another one, or writing many functions with the same type signature, consider whether you can apply a type declaration to entire functions, rather than repeating types of parameters and return values.
考虑将类型注释应用于整个函数表达式,而不是它们的参数和返回类型。
Consider applying type annotations to entire function expressions, rather than to their parameters and return type.
如果您重复编写相同的类型签名,请分解出一种函数类型或寻找现有的类型。如果您是图书馆作者,请提供常见回调的类型。
If you’re writing the same type signature repeatedly, factor out a function type or look for an existing one. If you’re a library author, provide types for common callbacks.
如果你想在 TypeScript 中定义一个命名类型,你有两个选择。您可以使用类型,如下所示:
If you want to define a named type in TypeScript, you have two options. You can use a type, as shown here:
typeTState={name:string;capital:string;}
typeTState={name:string;capital:string;}
或接口:
or an interface:
interfaceIState{name:string;capital:string;}
interfaceIState{name:string;capital:string;}
(您也可以使用 a class,但这是一个 JavaScript 运行时概念,它也引入了一个值。请参阅第 8 项。)
(You could also use a class, but that is a JavaScript runtime concept that also introduces a value. See Item 8.)
你应该使用哪个,type或者interface?多年来,这两个选项之间的界限变得越来越模糊,以至于在许多情况下您可以使用其中一个。您应该了解在哪种情况下使用哪种语言之间的区别,并保持一致type。interface但是您还应该知道如何使用两者编写相同的类型,这样您就可以轻松阅读使用其中任何一种的 TypeScript。
Which should you use, type or interface? The line between these two options has become increasingly blurred over the years, to the point that in many situations you can use either. You should be aware of the distinctions that remain between type and interface and be consistent about which you use in which situation. But you should also know how to write the same types using both, so that you’ll be comfortable reading TypeScript that uses either.
此项中的示例在类型名称前加上I或T仅表示它们是如何定义的。你不应该在你的代码中这样做!在 C# 中给接口类型加上前缀I很常见,这种约定在早期的 TypeScript 中取得了一些进展。但是今天它被认为是糟糕的风格,因为它是不必要的,增加的价值很小,并且在标准库中没有得到一致的遵循。
The examples in this item prefix type names with I or T solely to indicate how they were defined. You should not do this in your code! Prefixing interface types with I is common in C#, and this convention made some inroads in the early days of TypeScript. But it is considered bad style today because it’s unnecessary, adds little value, and is not consistently followed in the standard libraries.
首先,相似之处:状态类型彼此之间几乎没有区别。如果您定义一个IState或一个TState具有额外属性的值,您得到的错误是逐个字符相同的:
First, the similarities: the State types are nearly indistinguishable from one another. If you define an IState or a TState value with an extra property, the errors you get are character-by-character identical:
constwyoming:TState={name:'Wyoming',capital:'Cheyenne',population:500_000// ~~~~~~~~~~~~~~~~~~ Type ... is not assignable to type 'TState'// Object literal may only specify known properties, and// 'population' does not exist in type 'TState'};
constwyoming:TState={name:'Wyoming',capital:'Cheyenne',population:500_000// ~~~~~~~~~~~~~~~~~~ Type ... is not assignable to type 'TState'// Object literal may only specify known properties, and// 'population' does not exist in type 'TState'};
您可以将索引签名与interface和一起使用type:
You can use an index signature with both interface and type:
typeTDict={[key:string]:string};interfaceIDict{[key:string]:string;}
typeTDict={[key:string]:string};interfaceIDict{[key:string]:string;}
您还可以使用以下任一方式定义函数类型:
You can also define function types with either:
typeTFn=(x:number)=>string;interfaceIFn{(x:number):string;}consttoStrT:TFn=x=>''+x;// OKconsttoStrI:IFn=x=>''+x;// OK
typeTFn=(x:number)=>string;interfaceIFn{(x:number):string;}consttoStrT:TFn=x=>''+x;// OKconsttoStrI:IFn=x=>''+x;// OK
对于这个简单的函数类型,类型别名看起来更自然,但如果该类型也有属性,那么声明开始看起来更相似:
The type alias looks more natural for this straightforward function type, but if the type has properties as well, then the declarations start to look more alike:
typeTFnWithProperties={(x::::number):number;prop_string;}interfaceIFnWithProperties{(x_number):number;prop_string;}
typeTFnWithProperties={(x:number):number;prop:string;}interfaceIFnWithProperties{(x:number):number;prop:string;}
您可以通过提醒自己在 JavaScript 中函数是可调用对象来记住此语法。
You can remember this syntax by reminding yourself that in JavaScript, functions are callable objects.
类型别名和接口都可以是通用的:
Both type aliases and interfaces can be generic:
typeTPair<T>={first::::T;second_T;}interfaceIPair<T>{first_T;second_T;}
typeTPair<T>={first:T;second:T;}interfaceIPair<T>{first:T;second:T;}
Aninterface可以扩展 a type(有一些注意事项,暂时解释),并且 atype可以扩展 an interface:
An interface can extend a type (with some caveats, explained momentarily), and a type can extend an interface:
interfaceIStateWithPopextendsTState{population:number;}typeTStateWithPop=IState&{population:number;};
interfaceIStateWithPopextendsTState{population:number;}typeTStateWithPop=IState&{population:number;};
同样,这些类型是相同的。需要注意的是 aninterface不能扩展像 a 这样的复杂类型工会类型。如果你想这样做,你需要使用typeand &。
Again, these types are identical. The caveat is that an interface cannot extend a complex type like a union type. If you want to do that, you’ll need to use type and &.
A class can implement either an interface or a simple type:
classStateTimplementsTState{name::::string='';capital_string='';}classStateIimplementsIState{name_string='';capital_string='';}
classStateTimplementsTState{name:string='';capital:string='';}classStateIimplementsIState{name:string='';capital:string='';}
这些就是相似之处。差异呢?你已经看过了——有 uniontype但没有 union interface:
Those are the similarities. What about the differences? You’ve seen one already—there are union types but no union interfaces:
typeAorB='a'|'b';
typeAorB='a'|'b';
扩展联合类型可能很有用。如果您有单独的类型Input和Output变量以及从名称到变量的映射:
Extending union types can be useful. If you have separate types for Input and Output variables and a mapping from name to variable:
typeInput={/* ... */};typeOutput={/* ... */};interfaceVariableMap{[name:string]:Input|Output;}
typeInput={/* ... */};typeOutput={/* ... */};interfaceVariableMap{[name:string]:Input|Output;}
那么您可能需要一个将名称附加到变量的类型。这将是:
then you might want a type that attaches the name to the variable. This would be:
typeNamedVariable=(Input|Output)&{name:string};
typeNamedVariable=(Input|Output)&{name:string};
这种类型不能用 来表达interface。Atype通常比interface. 它可以是联合,也可以利用更高级的特性,如映射类型或条件类型。
This type cannot be expressed with interface. A type is, in general, more capable than an interface. It can be a union, and it can also take advantage of more advanced features like mapped or conditional types.
它还可以更轻松地表达元组和数组类型:
It can also more easily express tuple and array types:
typePair=[number,number];typeStringList=string[];typeNamedNums=[string,...number[]];
typePair=[number,number];typeStringList=string[];typeNamedNums=[string,...number[]];
您可以使用以下方式表达类似元组的内容interface:
You can express something like a tuple using interface:
interfaceTuple{0::::number;1_number;length_2;}constt_Tuple=[10,20];// OK
interfaceTuple{0:number;1:number;length:2;}constt:Tuple=[10,20];// OK
但这很尴尬,并且会丢弃所有元组方法,例如concat. 最好使用一个type. 有关数字索引问题的更多信息,请参阅条目 16。
But this is awkward and drops all the tuple methods like concat. Better to use a type. For more on the problems of numeric indices, see Item 16.
一个 interface然而,确实有一些 atype没有的能力。其中之一是 aninterface可以被扩充。回到这个例子,你可以用另一种方式State添加一个字段:population
An interface does have some abilities that a type doesn’t, however. One of these is that an interface can be augmented. Going back to the State example, you could have added a population field in another way:
interfaceIState{name::::::string;capital_string;}interfaceIState{population_number;}constwyoming_IState={name:'Wyoming',capital:'Cheyenne',population_500_000};// OK
interfaceIState{name:string;capital:string;}interfaceIState{population:number;}constwyoming:IState={name:'Wyoming',capital:'Cheyenne',population:500_000};// OK
这被称为“声明合并”,如果您以前从未见过它,那将是相当令人惊讶的。这主要与类型声明文件(第 6 章)一起使用,如果你正在编写一个,你应该遵循规范并使用interface它来支持它。这个想法是,您的类型声明中可能存在用户需要填写的空白,他们就是这样做的。
This is known as “declaration merging,” and it’s quite surprising if you’ve never seen it before. This is primarily used with type declaration files (Chapter 6), and if you’re writing one, you should follow the norms and use interface to support it. The idea is that there may be gaps in your type declarations that users need to fill, and this is how they do it.
TypeScript 使用合并为不同版本的 JavaScript 标准库获取不同的类型。Array例如,接口在lib.es5.d.ts中定义。默认情况下,这就是您所得到的。但是如果你添加ES2015到lib你的tsconfig.json的条目,TypeScript 也会包含lib.es2015.d.ts。这包括另一个Array带有附加方法的接口,就像find在 ES2015 中添加的那样。它们通过合并被添加到另一个Array界面。最终效果是您获得了Array具有完全正确方法的单一类型。
TypeScript uses merging to get different types for the different versions of JavaScript’s standard library. The Array interface, for example, is defined in lib.es5.d.ts. By default this is all you get. But if you add ES2015 to the lib entry of your tsconfig.json, TypeScript will also include lib.es2015.d.ts. This includes another Array interface with additional methods like find that were added in ES2015. They get added to the other Array interface via merging. The net effect is that you get a single Array type with exactly the right methods.
常规代码和声明都支持合并,您应该意识到这种可能性。如果没有人增加你的类型是很重要的,那么使用type.
Merging is supported in regular code as well as declarations, and you should be aware of the possibility. If it’s essential that no one ever augment your type, then use type.
回到项目开头的问题,你应该使用typeorinterface吗?对于复杂类型,您别无选择:您需要使用类型别名。但是可以用任何一种方式表示的更简单的对象类型呢?要回答这个问题,您应该考虑一致性和扩充。您是否在一贯使用的代码库中工作interface?然后坚持下去interface。它使用吗type?然后使用type。
Returning to the question at the start of the item, should you use type or interface? For complex types, you have no choice: you need to use a type alias. But what about the simpler object types that can be represented either way? To answer this question, you should consider consistency and augmentation. Are you working in a codebase that consistently uses interface? Then stick with interface. Does it use type? Then use type.
对于没有既定风格的项目,您应该考虑扩充。您要发布 API 的类型声明吗?interface然后,当 API 更改时,您的用户能够通过合并新字段可能会有所帮助。所以使用interface. 但是对于项目内部使用的类型,声明合并很可能是错误的。所以更喜欢type。
For projects without an established style, you should think about augmentation. Are you publishing type declarations for an API? Then it might be helpful for your users to be able to be able to merge in new fields via an interface when the API changes. So use interface. But for a type that’s used internally in your project, declaration merging is likely to be a mistake. So prefer type.
This script prints the dimensions, surface areas, and volumes of a few cylinders:
console.log('Cylinder 1 x 1 ','Surface area:',6.283185*1*1+6.283185*1*1,'Volume:',3.14159*1*1*1);console.log('Cylinder 1 x 2 ','Surface area:',6.283185*1*1+6.283185*2*1,'Volume:',3.14159*1*2*1);console.log('Cylinder 2 x 1 ','Surface area:',6.283185*2*1+6.283185*2*1,'Volume:',3.14159*2*2*1);
console.log('Cylinder 1 x 1 ','Surface area:',6.283185*1*1+6.283185*1*1,'Volume:',3.14159*1*1*1);console.log('Cylinder 1 x 2 ','Surface area:',6.283185*1*1+6.283185*2*1,'Volume:',3.14159*1*2*1);console.log('Cylinder 2 x 1 ','Surface area:',6.283185*2*1+6.283185*2*1,'Volume:',3.14159*2*2*1);
这段代码看着不舒服吗?它应该是。这是极其重复的,就好像同一行被复制粘贴,然后修改。它重复值和常量。这导致了一个错误的出现(你发现了吗?)。更好的方法是分解出一些函数、一个常量和一个循环:
Is this code uncomfortable to look at? It should be. It’s extremely repetitive, as though the same line was copied and pasted, then modified. It repeats both values and constants. This has allowed an error to creep in (did you spot it?). Much better would be to factor out some functions, a constant, and a loop:
constsurfaceArea=(r,h)=>2*Math.PI*r*(r+h);constvolume=(r,h)=>Math.PI*r*r*h;for(const[r,h]of[[1,1],[1,2],[2,1]]){console.log(`Cylinder${r}x${h}`,`Surface area:${surfaceArea(r,h)}`,`Volume:${volume(r,h)}`);}
constsurfaceArea=(r,h)=>2*Math.PI*r*(r+h);constvolume=(r,h)=>Math.PI*r*r*h;for(const[r,h]of[[1,1],[1,2],[2,1]]){console.log(`Cylinder${r}x${h}`,`Surface area:${surfaceArea(r,h)}`,`Volume:${volume(r,h)}`);}
这就是 DRY 原则:不要重复自己。这是您在软件开发中能找到的最接近通用建议的东西。然而,努力避免代码重复的开发人员可能不会在类型方面三思而后行:
This is the DRY principle: don’t repeat yourself. It’s the closest thing to universal advice that you’ll find in software development. Yet developers who assiduously avoid repetition in code may not think twice about it in types:
interfacePerson{firstName:string;lastName:string;}interfacePersonWithBirthDate{firstName:string;lastName:string;birth:Date;}
interfacePerson{firstName:string;lastName:string;}interfacePersonWithBirthDate{firstName:string;lastName:string;birth:Date;}
类型中的重复与代码中的重复有许多相同的问题。middleName如果您决定向 中添加可选字段怎么办Person?现在Person和PersonWithBirthDate已经分道扬镳了。
Duplication in types has many of the same problems as duplication in code. What if you decide to add an optional middleName field to Person? Now Person and PersonWithBirthDate have diverged.
重复在类型中更常见的一个原因是分解出共享模式的机制不如它们对代码熟悉:什么是分解出辅助函数的类型系统等价物?通过学习如何在类型之间进行映射,您可以将 DRY 的好处带到您的类型定义中。
One reason that duplication is more common in types is that the mechanisms for factoring out shared patterns are less familiar than they are with code: what’s the type system equivalent of factoring out a helper function? By learning how to map between types, you can bring the benefits of DRY to your type definitions.
减少重复的最简单方法是命名您的类型。而不是以这种方式编写距离函数:
The simplest way to reduce repetition is by naming your types. Rather than writing a distance function this way:
functiondistance(a:{x::::number,y_number},b:{x_number,y_number}){returnMath.sqrt(Math.pow(a.x-b.x,2)+Math.pow(a.y-b.y,2));}
functiondistance(a:{x:number,y:number},b:{x:number,y:number}){returnMath.sqrt(Math.pow(a.x-b.x,2)+Math.pow(a.y-b.y,2));}
为类型创建一个名称并使用它:
create a name for the type and use that:
interfacePoint2D{x::::number;y_number;}functiondistance(a_Point2D,b_Point2D){/* ... */}
interfacePoint2D{x:number;y:number;}functiondistance(a:Point2D,b:Point2D){/* ... */}
这是类型系统等同于分解出一个常量而不是重复写入它。重复的类型并不总是那么容易发现。有时它们会被语法掩盖。如果多个函数共享相同的类型签名,例如:
This is the type system equivalent of factoring out a constant instead of writing it repeatedly. Duplicated types aren’t always so easy to spot. Sometimes they can be obscured by syntax. If several functions share the same type signature, for instance:
functionget(url::::string,opts_Options):Promise<Response>{/* ... */}functionpost(url_string,opts_Options):Promise<Response>{/* ... */}
functionget(url:string,opts:Options):Promise<Response>{/* ... */}functionpost(url:string,opts:Options):Promise<Response>{/* ... */}
然后你可以为这个签名分解出一个命名类型:
Then you can factor out a named type for this signature:
typeHTTPFunction=(url::::string,opts_Options)=>Promise<Response>;constget_HTTPFunction=(url,opts)=>{/* ... */};constpost_HTTPFunction=(url,opts)=>{/* ... */};
typeHTTPFunction=(url:string,opts:Options)=>Promise<Response>;constget:HTTPFunction=(url,opts)=>{/* ... */};constpost:HTTPFunction=(url,opts)=>{/* ... */};
有关更多信息,请参阅第 12 项。
For more on this, see Item 12.
什么关于Person/PersonWithBirthDate例子?您可以通过使一个接口扩展另一个接口来消除重复:
What about the Person/PersonWithBirthDate example? You can eliminate the repetition by making one interface extend the other:
interfacePerson{firstName:string;lastName:string;}interfacePersonWithBirthDateextendsPerson{birth:Date;}
interfacePerson{firstName:string;lastName:string;}interfacePersonWithBirthDateextendsPerson{birth:Date;}
现在你只需要写额外的字段。如果这两个接口共享它们字段的一个子集,那么您可以分解出一个仅包含这些公共字段的基类。继续与代码重复进行类比,这类似于编写PIand2*PI而不是3.141593and 6.283185。
Now you only need to write the additional fields. If the two interfaces share a subset of their fields, then you can factor out a base class with just these common fields. Continuing the analogy with code duplication, this is akin to writing PI and 2*PI instead of 3.141593 and 6.283185.
您还可以使用交集运算符 ( &) 来扩展现有类型,尽管这种情况不太常见:
You can also use the intersection operator (&) to extend an existing type, though this is less common:
typePersonWithBirthDate=Person&{birth:Date};
typePersonWithBirthDate=Person&{birth:Date};
当您想向联合类型添加一些附加属性(您不能这样做extend)时,此技术最有用。有关更多信息,请参阅第 13 项。
This technique is most useful when you want to add some additional properties to a union type (which you cannot extend). For more on this, see Item 13.
你也可以往另一个方向走。如果您有一个类型 ,State它代表整个应用程序的状态,而另一个类型 ,TopNavState它只代表一部分,该怎么办?
You can also go the other direction. What if you have a type, State, which represents the state of an entire application, and another, TopNavState, which represents just a part?
interfaceState{userId::::::::string;pageTitle_string;recentFiles_string[];pageContents_string;}interfaceTopNavState{userId_string;pageTitle_string;recentFiles_string[];}
interfaceState{userId:string;pageTitle:string;recentFiles:string[];pageContents:string;}interfaceTopNavState{userId:string;pageTitle:string;recentFiles:string[];}
State与其通过扩展构建TopNavState,不如将 定义TopNavState为 中字段的子集State。通过这种方式,您可以保留一个定义整个应用程序状态的界面。
Rather than building up State by extending TopNavState, you’d like to define TopNavState as a subset of the fields in State. This way you can keep a single interface defining the state for the entire app.
You can remove duplication in the types of the properties by indexing into State:
typeTopNavState={userId:State['userId'];pageTitle:State['pageTitle'];recentFiles:State['recentFiles'];};
typeTopNavState={userId:State['userId'];pageTitle:State['pageTitle'];recentFiles:State['recentFiles'];};
尽管它更长了,这是pageTitle进步: in类型的变化State将反映在TopNavState. 但它仍然是重复的。您可以使用映射类型做得更好:
While it’s longer, this is progress: a change in the type of pageTitle in State will get reflected in TopNavState. But it’s still repetitive. You can do better with a mapped type:
typeTopNavState={[kin'userId'|'pageTitle'|'recentFiles']:State[k]};
typeTopNavState={[kin'userId'|'pageTitle'|'recentFiles']:State[k]};
将鼠标悬停在上面TopNavState表明这个定义实际上与前一个定义完全相同(见图2-10)。
Mousing over TopNavState shows that this definition is, in fact, exactly the same as the previous one (see Figure 2-10).
映射类型是类型系统,等同于遍历数组中的字段。这种特殊模式非常常见,它是标准库的一部分,它被称为Pick:
Mapped types are the type system equivalent of looping over the fields in an array. This particular pattern is so common that it’s part of the standard library, where it’s called Pick:
typePick<T,K>={[kinK]:T[k]};
typePick<T,K>={[kinK]:T[k]};
(这个定义并不十分完整,正如您将看到的那样。)您可以像这样使用它:
(This definition isn’t quite complete, as you will see.) You use it like this:
typeTopNavState=Pick<State,'userId'|'pageTitle'|'recentFiles'>;
typeTopNavState=Pick<State,'userId'|'pageTitle'|'recentFiles'>;
Pick是泛型类型的一个例子。继续类比去除代码重复,usingPick相当于调用一个函数。Pick接受两种类型,Tand K,并返回第三种类型,就像一个函数可能接受两个值并返回第三种类型一样。
Pick is an example of a generic type. Continuing the analogy to removing code duplication, using Pick is the equivalent of calling a function. Pick takes two types, T and K, and returns a third, much as a function might take two values and return a third.
另一种形式的重复可能出现在标记联合中。如果你只想要标签的类型怎么办?
Another form of duplication can arise with tagged unions. What if you want a type for just the tag?
interfaceSaveAction{type:'save';// ...}interfaceLoadAction{type:'load';// ...}typeAction=SaveAction|LoadAction;typeActionType='save'|'load';// Repeated types!
interfaceSaveAction{type:'save';// ...}interfaceLoadAction{type:'load';// ...}typeAction=SaveAction|LoadAction;typeActionType='save'|'load';// Repeated types!
您可以ActionType通过索引联合来定义而无需重复自己Action:
You can define ActionType without repeating yourself by indexing into the Action union:
typeActionType=Action['type'];// Type is "save" | "load"
typeActionType=Action['type'];// Type is "save" | "load"
当您向Action联合添加更多类型时,ActionType将自动合并它们。这种类型与您使用的类型不同Pick,后者会为您提供一个具有属性的接口type:
As you add more types to the Action union, ActionType will incorporate them automatically. This type is distinct from what you’d get using Pick, which would give you an interface with a type property:
typeActionRec=Pick<Action,'type'>;// {type: "save" | "load"}
typeActionRec=Pick<Action,'type'>;// {type: "save" | "load"}
如果您正在定义一个可以初始化并稍后更新的类,则更新方法的参数类型将选择性地包括与构造函数相同的大部分参数:
If you’re defining a class which can be initialized and later updated, the type for the parameter to the update method will optionally include most of the same parameters as the constructor:
interfaceOptions{width:::::::::::number;height_number;color_string;label_string;}interfaceOptionsUpdate{width?_number;height?_number;color?_string;label?_string;}classUIWidget{constructor(init_Options){/* ... */}update(options_OptionsUpdate){/* ... */}}
interfaceOptions{width:number;height:number;color:string;label:string;}interfaceOptionsUpdate{width?:number;height?:number;color?:string;label?:string;}classUIWidget{constructor(init:Options){/* ... */}update(options:OptionsUpdate){/* ... */}}
你可以OptionsUpdate使用Options映射类型构造并且keyof:
You can construct OptionsUpdate from Options using a mapped type and keyof:
typeOptionsUpdate={[kinkeyofOptions]?:Options[k]};
typeOptionsUpdate={[kinkeyofOptions]?:Options[k]};
keyof采用一个类型并为您提供其键类型的联合:
keyof takes a type and gives you a union of the types of its keys:
typeOptionsKeys=keyofOptions;// Type is "width" | "height" | "color" | "label"
typeOptionsKeys=keyofOptions;// Type is "width" | "height" | "color" | "label"
映射类型 ( [k in keyof Options]) 迭代这些并在 中查找相应的值类型Options。这?使得每个属性都是可选的。这pattern 也非常常见,并在标准库中作为Partial:
The mapped type ([k in keyof Options]) iterates over these and looks up the corresponding value type in Options. The ? makes each property optional. This pattern is also extremely common and is enshrined in the standard library as Partial:
classUIWidget{constructor(init:Options){/* ... */}update(options:Partial<Options>){/* ... */}}
classUIWidget{constructor(init:Options){/* ... */}update(options:Partial<Options>){/* ... */}}
您可能还会发现自己想要定义一个与值的形状相匹配的类型:
You may also find yourself wanting to define a type that matches the shape of a value:
constINIT_OPTIONS={width:::::::640,height_480,color:'#00FF00',label:'VGA',};interfaceOptions{width_number;height_number;color_string;label_string;}
constINIT_OPTIONS={width:640,height:480,color:'#00FF00',label:'VGA',};interfaceOptions{width:number;height:number;color:string;label:string;}
typeOptions=typeofINIT_OPTIONS;
typeOptions=typeofINIT_OPTIONS;
这有意唤起 JavaScript 的运行时运typeof算符,但它在 TypeScript 类型级别运行,并且更加精确。有关更多信息typeof,请参阅第 8 项。但是,从值派生类型时要小心。通常最好先定义类型并声明值可以分配给它们。这使您的类型更加明确,并且更少受变幻莫测的扩大(Item 21)的影响。
This intentionally evokes JavaScript’s runtime typeof operator, but it operates at the level of TypeScript types and is much more precise. For more on typeof, see Item 8. Be careful about deriving types from values, however. It’s usually better to define types first and declare that values are assignable to them. This makes your types more explicit and less subject to the vagaries of widening (Item 21).
同样,您可能希望为函数或方法的推断返回值创建命名类型:
Similarly, you may want to create a named type for the inferred return value of a function or method:
functiongetUserInfo(userId:string){// ...return{userId,name,age,height,weight,favoriteColor,};}// Return type inferred as { userId: string; name: string; age: number, ... }
functiongetUserInfo(userId:string){// ...return{userId,name,age,height,weight,favoriteColor,};}// Return type inferred as { userId: string; name: string; age: number, ... }
直接这样做需要条件类型(参见条目 50)。但是,正如我们之前所见,标准库为像这样的常见模式定义了泛型类型。在这个如果ReturnType泛型完全符合您的要求:
Doing this directly requires conditional types (see Item 50). But, as we’ve seen before, the standard library defines generic types for common patterns like this one. In this case the ReturnType generic does exactly what you want:
typeUserInfo=ReturnType<typeofgetUserInfo>;
typeUserInfo=ReturnType<typeofgetUserInfo>;
请注意,ReturnType操作typeof getUserInfo函数的类型,而不是getUserInfo函数的值。与 一样typeof,明智地使用此技术。不要混淆你的真相来源。
Note that ReturnType operates on typeof getUserInfo, the function’s type, rather than getUserInfo, the function’s value. As with typeof, use this technique judiciously. Don’t get mixed up about your source of truth.
通用类型等同于类型的函数。函数是逻辑 DRY 的关键。因此,泛型是类型 DRY 的关键也就不足为奇了。但是这个类比有一个缺失的部分。您使用类型系统来限制您可以使用函数映射的值:您添加数字,而不是对象;您找到形状的区域,而不是数据库记录。你如何约束泛型类型中的参数?
Generic types are the equivalent of functions for types. And functions are the key to DRY for logic. So it should come as no surprise that generics are the key to DRY for types. But there’s a missing piece to this analogy. You use the type system to constrain the values you can map with a function: you add numbers, not objects; you find the area of shapes, not database records. How do you constrain the parameters in a generic type?
你这样做extends。您可以将任何泛型参数声明为extends一个类型。例如:
You do so with extends. You can declare that any generic parameter extends a type. For example:
interfaceName{first:string;last:string;}typeDancingDuo<TextendsName>=[T,T];constcouple1:DancingDuo<Name>=[{first:'Fred',last:'Astaire'},{first:'Ginger',last:'Rogers'}];// OKconstcouple2:DancingDuo<{first:string}>=[// ~~~~~~~~~~~~~~~// Property 'last' is missing in type// '{ first: string; }' but required in type 'Name'{first:'Sonny'},{first:'Cher'}];
interfaceName{first:string;last:string;}typeDancingDuo<TextendsName>=[T,T];constcouple1:DancingDuo<Name>=[{first:'Fred',last:'Astaire'},{first:'Ginger',last:'Rogers'}];// OKconstcouple2:DancingDuo<{first:string}>=[// ~~~~~~~~~~~~~~~// Property 'last' is missing in type// '{ first: string; }' but required in type 'Name'{first:'Sonny'},{first:'Cher'}];
{first: string}不扩展Name,因此错误。
{first: string} does not extend Name, hence the error.
目前,TypeScript 总是要求你在声明中写出泛型参数。写作DancingDuo而不是DancingDuo<Name>不会削减它。如果你想让 TypeScript 推断泛型参数的类型,你可以使用一个仔细类型化的标识函数:
At the moment, TypeScript always requires you to write out the generic parameter in a declaration. Writing DancingDuo instead of DancingDuo<Name> won’t cut it. If you want TypeScript to infer the type of the generic parameter, you can use a carefully typed identity function:
constdancingDuo=<TextendsName>(x:DancingDuo<T>)=>x;constcouple1=dancingDuo([{first:'Fred',last:'Astaire'},{first:'Ginger',last:'Rogers'}]);constcouple2=dancingDuo([{first:'Bono'},// ~~~~~~~~~~~~~~{first:'Prince'}// ~~~~~~~~~~~~~~~~// Property 'last' is missing in type// '{ first: string; }' but required in type 'Name']);
constdancingDuo=<TextendsName>(x:DancingDuo<T>)=>x;constcouple1=dancingDuo([{first:'Fred',last:'Astaire'},{first:'Ginger',last:'Rogers'}]);constcouple2=dancingDuo([{first:'Bono'},// ~~~~~~~~~~~~~~{first:'Prince'}// ~~~~~~~~~~~~~~~~// Property 'last' is missing in type// '{ first: string; }' but required in type 'Name']);
为了一个特别有用的变体,参见inferringPick项目26。
For a particularly useful variation on this, see inferringPick in Item 26.
您可以使用来完成前面的extends定义。Pick如果你通过类型检查器运行原始版本,你会得到一个错误:
You can use extends to complete the definition of Pick from earlier. If you run the original version through the type checker, you get an error:
typePick<T,K>={[kinK]:T[k]// ~ Type 'K' is not assignable to type 'string | number | symbol'};
typePick<T,K>={[kinK]:T[k]// ~ Type 'K' is not assignable to type 'string | number | symbol'};
K在这种类型中不受约束,显然过于宽泛:它需要是可以用作索引的东西,即string | number | symbol. 但你可以变得更窄——K实际上应该是 T 的键的某个子集,即keyof T:
K is unconstrained in this type and is clearly too broad: it needs to be something that can be used as an index, namely, string | number | symbol. But you can get narrower than that—K should really be some subset of the keys of T, namely, keyof T:
typePick<T,KextendskeyofT>={[kinK]:T[k]};// OK
typePick<T,KextendskeyofT>={[kinK]:T[k]};// OK
将类型视为一组值(第 7 项),将此处的“扩展”理解为“的子集”会有所帮助。
Thinking of types as sets of values (Item 7), it helps to read “extends” as “subset of” here.
当您使用越来越抽象的类型时,请尽量不要忘记目标:接受有效程序并拒绝无效程序。在这种情况下,约束的结果是传递Pick错误的键会产生错误:
As you work with increasingly abstract types, try not to lose sight of the goal: accepting valid programs and rejecting invalid ones. In this case, the upshot of the constraint is that passing Pick the wrong key will produce an error:
typeFirstLast=Pick<Name,'first'|'last'>;// OKtypeFirstMiddle=Pick<Name,'first'|'middle'>;// ~~~~~~~~~~~~~~~~~~// Type '"middle"' is not assignable// to type '"first" | "last"'
typeFirstLast=Pick<Name,'first'|'last'>;// OKtypeFirstMiddle=Pick<Name,'first'|'middle'>;// ~~~~~~~~~~~~~~~~~~// Type '"middle"' is not assignable// to type '"first" | "last"'
重复和复制/粘贴编码在类型空间中和在值空间中一样糟糕。用于避免类型空间重复的结构可能不如用于程序逻辑的结构那么熟悉,但它们值得努力学习。不要重复自己!
Repetition and copy/paste coding are just as bad in type space as they are in value space. The constructs you use to avoid repetition in type space may be less familiar than those used for program logic, but they are worth the effort to learn. Don’t repeat yourself!
DRY(不要重复自己)原则既适用于类型,也适用于逻辑。
The DRY (don’t repeat yourself) principle applies to types as much as it applies to logic.
命名类型而不是重复它们。用于extends避免在界面中重复字段。
Name types rather than repeating them. Use extends to avoid repeating fields in interfaces.
了解 TypeScript 提供的用于在类型之间进行映射的工具。这些包括keyof、typeof、索引和映射类型。
Build an understanding of the tools provided by TypeScript to map between types. These include keyof, typeof, indexing, and mapped types.
通用类型等同于类型的函数。使用它们在类型之间映射而不是重复类型。用于extends约束泛型类型。
Generic types are the equivalent of functions for types. Use them to map between types instead of repeating types. Use extends to constrain generic types.
Familiarize yourself with generic types defined in the standard library such as Pick, Partial, and ReturnType.
一JavaScript 最好的特性之一是它创建对象的便捷语法:
One of the best features of JavaScript is its convenient syntax for creating objects:
constrocket={name:'Falcon 9',variant:'Block 5',thrust:'7,607 kN',};
constrocket={name:'Falcon 9',variant:'Block 5',thrust:'7,607 kN',};
JavaScript 中的对象将字符串键映射到任何类型的值。TypeScript 允许您通过在类型上指定索引签名来表示像这样的灵活映射:
Objects in JavaScript map string keys to values of any type. TypeScript lets you represent flexible mappings like this by specifying an index signature on the type:
typeRocket={[property:string]:string};constrocket:Rocket={name:'Falcon 9',variant:'v1.0',thrust:'4,940 kN',};// OK
typeRocket={[property:string]:string};constrocket:Rocket={name:'Falcon 9',variant:'v1.0',thrust:'4,940 kN',};// OK
是[property: string]: string索引签名。它规定了三件事:
The [property: string]: string is the index signature. It specifies three things:
这纯粹是为了文档;类型检查器不以任何方式使用它。
This is purely for documentation; it is not used by the type checker in any way.
这需要是 、 或 的某种组合string,number但symbol通常您只想使用string(请参阅第 16 项)。
This needs to be some combination of string, number, or symbol, but generally you just want to use string (see Item 16).
这可以是任何东西。
This can be anything.
虽然这会进行类型检查,但它有一些缺点:
While this does type check, it has a few downsides:
它允许任何密钥,包括不正确的密钥。如果你写的Name不是name,它仍然是一个有效的Rocket类型。
It allows any keys, including incorrect ones. Had you written Name instead of name, it would have still been a valid Rocket type.
它不需要任何特定的密钥。{}也是有效的Rocket。
It doesn’t require any specific keys to be present. {} is also a valid Rocket.
不同的键不能有不同的类型。例如,thrust可能应该是一个number,而不是一个string。
It cannot have distinct types for different keys. For example, thrust should probably be a number, not a string.
TypeScript 的语言服务无法帮助您处理此类类型。在您键入时name:,没有自动完成功能,因为键可以是任何内容。
TypeScript’s language services can’t help you with types like this. As you’re typing name:, there’s no autocomplete because the key could be anything.
在简而言之,索引签名不是很精确。几乎总是有更好的替代品。在这种情况下,Rocket显然应该是interface:
In short, index signatures are not very precise. There are almost always better alternatives to them. In this case, Rocket should clearly be an interface:
interfaceRocket{name::::::string;variant_string;thrust_kN_number;}constfalconHeavy_Rocket={name:'Falcon Heavy',variant:'v1',thrust_kN_15_200};
interfaceRocket{name:string;variant:string;thrust_kN:number;}constfalconHeavy:Rocket={name:'Falcon Heavy',variant:'v1',thrust_kN:15_200};
现在thrust_kN是 a number,TypeScript 将检查是否存在所有必填字段。TypeScript 提供的所有优秀语言服务都可用:自动完成、跳转到定义、重命名——它们都可以工作。
Now thrust_kN is a number and TypeScript will check for the presence of all required fields. All the great language services that TypeScript provides are available: autocomplete, jump to definition, rename—and they all work.
什么你应该使用索引签名吗?规范案例是真正的动态数据。这可能来自 CSV 文件,例如,您有一个标题行并希望将数据行表示为将列名映射到值的对象:
What should you use index signatures for? The canonical case is truly dynamic data. This might come from a CSV file, for instance, where you have a header row and want to represent data rows as objects mapping column names to values:
functionparseCSV(input:string):{[columnName:string]:string}[]{constlines=input.split('\n');const[header,...rows]=lines;returnrows.map(rowStr=>{constrow:{[columnName:string]:string}={};rowStr.split(',').forEach((cell,i)=>{row[header[i]]=cell;});returnrow;});}
functionparseCSV(input:string):{[columnName:string]:string}[]{constlines=input.split('\n');const[header,...rows]=lines;returnrows.map(rowStr=>{constrow:{[columnName:string]:string}={};rowStr.split(',').forEach((cell,i)=>{row[header[i]]=cell;});returnrow;});}
在这样的一般设置中,无法提前知道列名是什么。所以索引签名是合适的。如果 的用户parseCSV更了解特定上下文中的列是什么,他们可能希望使用断言来获取更具体的类型:
There’s no way to know in advance what the column names are in such a general setting. So an index signature is appropriate. If the user of parseCSV knows more about what the columns are in a particular context, they may want to use an assertion to get a more specific type:
interfaceProductRow{productId:string;name:string;price:string;}declareletcsvData:string;constproducts=parseCSV(csvData)asunknownasProductRow[];
interfaceProductRow{productId:string;name:string;price:string;}declareletcsvData:string;constproducts=parseCSV(csvData)asunknownasProductRow[];
的当然,不能保证运行时的列实际上符合您的期望。如果这是你关心的事情,你可以添加undefined到值类型:
Of course, there’s no guarantee that the columns at runtime will actually match your expectation. If this is something you’re concerned about, you can add undefined to the value type:
functionsafeParseCSV(input:string):{[columnName:string]:string|undefined}[]{returnparseCSV(input);}
functionsafeParseCSV(input:string):{[columnName:string]:string|undefined}[]{returnparseCSV(input);}
现在每次访问都需要检查:
Now every access requires a check:
constrows=parseCSV(csvData);constprices:{[produt:string]:number}={};for(constrowofrows){prices[row.productId]=Number(row.price);}constsafeRows=safeParseCSV(csvData);for(constrowofsafeRows){prices[row.productId]=Number(row.price);// ~~~~~~~~~~~~~ Type 'undefined' cannot be used as an index type}
constrows=parseCSV(csvData);constprices:{[produt:string]:number}={};for(constrowofrows){prices[row.productId]=Number(row.price);}constsafeRows=safeParseCSV(csvData);for(constrowofsafeRows){prices[row.productId]=Number(row.price);// ~~~~~~~~~~~~~ Type 'undefined' cannot be used as an index type}
当然,这可能会使类型使用起来不太方便。运用你的判断力。
Of course, this may make the type less convenient to work with. Use your judgment.
如果您的类型具有一组有限的可能字段,请不要使用索引签名对其进行建模。例如,如果您知道您的数据将具有 A、B、C、D 等键,但您不知道其中有多少,则可以使用可选字段或联合对类型进行建模:
If your type has a limited set of possible fields, don’t model this with an index signature. For instance, if you know your data will have keys like A, B, C, D, but you don’t know how many of them there will be, you could model the type either with optional fields or a union:
interfaceRow1{[column::::::::::::::::string]:number}// Too broadinterfaceRow2{a_number;b?_number;c?_number;d?_number}// BettertypeRow3=|{a_number;}|{a_number;b_number;}|{a_number;b_number;c_number;}|{a_number;b_number;c_number;d_number};
interfaceRow1{[column:string]:number}// Too broadinterfaceRow2{a:number;b?:number;c?:number;d?:number}// BettertypeRow3=|{a:number;}|{a:number;b:number;}|{a:number;b:number;c:number;}|{a:number;b:number;c:number;d:number};
最后一种形式是最精确的,但使用起来可能不太方便。
The last form is the most precise, but it may be less convenient to work with.
如果使用索引签名的问题是范围string太广,那么有一些替代方案。
If the problem with using an index signature is that string is too broad, then there are a few alternatives.
一正在使用Record。这是一种通用类型,可让您在键类型方面更加灵活。特别是,您可以传入以下子集string:
One is using Record. This is a generic type that gives you more flexibility in the key type. In particular, you can pass in subsets of string:
typeVec3D=Record<'x'|'y'|'z',number>;// Type Vec3D = {// x: number;// y: number;// z: number;// }
typeVec3D=Record<'x'|'y'|'z',number>;// Type Vec3D = {// x: number;// y: number;// z: number;// }
Another is using a mapped type. This gives you the possibility of using different types for different keys:
typeVec3D={[kin'x'|'y'|'z']:number};// Same as abovetypeABC={[kin'a'|'b'|'c']:kextends'b'?string:number};// Type ABC = {// a: number;// b: string;// c: number;// }
typeVec3D={[kin'x'|'y'|'z']:number};// Same as abovetypeABC={[kin'a'|'b'|'c']:kextends'b'?string:number};// Type ABC = {// a: number;// b: string;// c: number;// }
当对象的属性直到运行时才知道时使用索引签名——例如,如果您从 CSV 文件加载它们。
Use index signatures when the properties of an object cannot be known until runtime—for example, if you’re loading them from a CSV file.
考虑添加undefined到索引签名的值类型以实现更安全的访问。
Consider adding undefined to the value type of an index signature for safer access.
Prefer more precise types to index signatures when possible: interfaces, Records, or mapped types.
JavaScript是一种著名的古怪语言。一些最臭名昭著的怪癖涉及隐式类型强制转换:
JavaScript is a famously quirky language. Some of the most notorious quirks involve implicit type coercions:
> “0” == 0 真
> "0" == 0 true
但是这些通常可以通过使用===and!==而不是它们更具强制性的表兄弟来避免。
but these can usually be avoided by using === and !== instead of their more coercive cousins.
JavaScript 的对象模型也有它的怪癖,理解这些更重要,因为其中一些是由 TypeScript 的类型系统建模的。您已经在第 10 项中看到了这样一个怪癖,它讨论了对象包装器类型。这个项目讨论另一个。
JavaScript’s object model also has its quirks, and these are more important to understand because some of them are modeled by TypeScript’s type system. You’ve already seen one such quirk in Item 10, which discussed object wrapper types. This item discusses another.
什么是一个对象?在 JavaScript 中,它是键/值对的集合。这键通常是字符串(在 ES2015 和更高版本中它们也可以是符号)。值可以是任何东西。
What is an object? In JavaScript it’s a collection of key/value pairs. The keys are ususally strings (in ES2015 and later they can also be symbols). The values can be anything.
这比您在许多其他语言中找到的限制更多。JavaScript 没有像 Python 或 Java 中那样的“可散列”对象的概念。如果您尝试使用更复杂的对象作为键,则通过调用其方法将其转换为字符串toString:
This is more restrictive than what you find in many other languages. JavaScript does not have a notion of “hashable” objects like you find in Python or Java. If you try to use a more complex object as a key, it is converted into a string by calling its toString method:
> x = {}
{}
> x[[1, 2, 3]] = 2
2个
> ×
{ '1,2,3': 1 }
> x = {}
{}
> x[[1, 2, 3]] = 2
2
> x
{ '1,2,3': 1 }
特别是,数字不能用作键。如果您尝试使用数字作为属性名称,JavaScript 运行时会将其转换为字符串:
In particular, numbers cannot be used as keys. If you try to use a number as a property name, the JavaScript runtime will convert it to a string:
> { 1: 2, 3: 4}
{ '1': 2, '3': 4 }> { 1: 2, 3: 4}
{ '1': 2, '3': 4 }
So what are arrays, then? They are certainly objects:
> typeof [] '对象'
> typeof [] 'object'
然而,对它们使用数字索引是很正常的:
And yet it’s quite normal to use numeric indices with them:
> x = [1, 2, 3] [ 1, 2, 3 ] > x[0] 1
> x = [1, 2, 3] [ 1, 2, 3 ] > x[0] 1
这些是否被转换成字符串?在最奇怪的怪癖之一中,答案是“是的”。您还可以使用字符串键访问数组的元素:
Are these being converted into strings? In one of the oddest quirks of all, the answer is “yes.” You can also access the elements of an array using string keys:
> x['1'] 2
> x['1'] 2
如果你用来Object.keys列出数组的键,你会得到字符串:
If you use Object.keys to list the keys of an array, you get strings back:
> Object.keys(x) [ '0', '1', '2' ]
> Object.keys(x) [ '0', '1', '2' ]
TypeScript 尝试通过允许数字键并区分这些键和字符串来为此带来一些理智。如果你深入研究Array( Item 6 ) 的类型声明,你会在lib.es5.d.ts中找到它:
TypeScript attempts to bring some sanity to this by allowing numeric keys and distinguishing between these and strings. If you dig into the type declarations for Array (Item 6), you’ll find this in lib.es5.d.ts:
interfaceArray<T>{// ...[n:number]:T;}
interfaceArray<T>{// ...[n:number]:T;}
这纯粹是虚构的——字符串键在运行时被接受,因为 ECMAScript 标准规定它们必须——但它是一个有助于发现错误的方法:
This is purely a fiction—string keys are accepted at runtime as the ECMAScript standard dictates that they must—but it is a helpful one that can catch mistakes:
constxs=[1,2,3];constx0=xs[0];// OKconstx1=xs['1'];// ~~~ Element implicitly has an 'any' type// because index expression is not of type 'number'functionget<T>(array:T[],k:string):T{returnarray[k];// ~ Element implicitly has an 'any' type// because index expression is not of type 'number'}
constxs=[1,2,3];constx0=xs[0];// OKconstx1=xs['1'];// ~~~ Element implicitly has an 'any' type// because index expression is not of type 'number'functionget<T>(array:T[],k:string):T{returnarray[k];// ~ Element implicitly has an 'any' type// because index expression is not of type 'number'}
虽然这本小说很有帮助,但请务必记住它只是小说。与 TypeScript 类型系统的所有方面一样,它在运行时被擦除(Item 3)。这意味着构造 likeObject.keys仍然返回字符串:
While this fiction is helpful, it’s important to remember that it is just a fiction. Like all aspects of TypeScript’s type system, it is erased at runtime (Item 3). This means that constructs like Object.keys still return strings:
constkeys=Object.keys(xs);// Type is string[]for(constkeyinxs){key;// Type is stringconstx=xs[key];// Type is number}
constkeys=Object.keys(xs);// Type is string[]for(constkeyinxs){key;// Type is stringconstx=xs[key];// Type is number}
最后一次访问有效有点令人惊讶,因为string它不能分配给number. 最好将其视为对这种遍历数组的风格的务实让步,这在 JavaScript 中很常见。这并不是说这是遍历数组的好方法。如果你不关心索引,你可以用于:
That this last access works is somewhat surprising since string is not assignable to number. It’s best thought of as a pragmatic concession to this style of iterating over arrays, which is common in JavaScript. That’s not to say that this is a good way to loop over an array. If you don’t care about the index, you can use for-of:
for(constxofxs){x;// Type is number}
for(constxofxs){x;// Type is number}
如果您确实关心索引,则可以使用Array.prototype.forEach,它将它作为一个提供给您number:
If you do care about the index, you can use Array.prototype.forEach, which gives it to you as a number:
xs.forEach((x,i)=>{i;// Type is numberx;// Type is number});
xs.forEach((x,i)=>{i;// Type is numberx;// Type is number});
如果你需要尽早跳出循环,你最好使用 C 风格的for(;;)循环:
If you need to break out of the loop early, you’re best off using a C-style for(;;) loop:
for(leti=0;i<xs.length;i++){constx=xs[i];if(x<0)break;}
for(leti=0;i<xs.length;i++){constx=xs[i];if(x<0)break;}
如果类型不能说服你,也许性能会:在大多数浏览器和 JavaScript 引擎中,数组上的 for-in 循环比 for-of 或 C 风格的 for 循环慢几个数量级。
If the types don’t convince you, perhaps the performance will: in most browsers and JavaScript engines, for-in loops over arrays are several orders of magnitude slower than for-of or a C-style for loop.
这里的一般模式是number索引签名意味着您输入的内容必须是一个number(for-in 循环明显例外),但您得到的是一个string.
The general pattern here is that a number index signature means that what you put in has to be a number (with the notable exception of for-in loops), but what you get out is a string.
如果这听起来令人困惑,因为它确实如此!作为一般规则,没有太多理由使用 .number作为类型的索引签名而不是string. 如果您想指定将使用数字索引的内容,您可能想改用数组或元组类型。用作number索引类型可能会产生一种误解,即数字属性是 JavaScript 中的一个东西,无论是对你自己还是对代码的读者。
If this sounds confusing, it’s because it is! As a general rule, there’s not much reason to use number as the index signature of a type rather than string. If you want to specify something that will be indexed using numbers, you probably want to use an Array or tuple type instead. Using number as an index type can create the misconception that numeric properties are a thing in JavaScript, either for yourself or for readers of your code.
如果您反对接受 Array 类型,因为它们有许多您可能不会使用的其他属性(来自它们的原型),例如pushand concat,那很好——您正在从结构上考虑!(如果你需要复习一下,请参阅第 4 项。)如果你真的想接受任何长度的元组或任何类似数组的结构,TypeScript有一个ArrayLike你可以使用的类型:
If you object to accepting an Array type because they have many other properties (from their prototype) that you might not use, such as push and concat, then that’s good—you’re thinking structurally! (If you need a refresher on this, refer to Item 4.) If you truly want to accept tuples of any length or any array-like construct, TypeScript has an ArrayLike type you can use:
functioncheckedAccess<T>(xs:ArrayLike<T>,i:number):T{if(i<xs.length){returnxs[i];}thrownewError(`Attempt to access${i}which is past end of array.`)}
functioncheckedAccess<T>(xs:ArrayLike<T>,i:number):T{if(i<xs.length){returnxs[i];}thrownewError(`Attempt to access${i}which is past end of array.`)}
这只有一个length数字索引签名。在极少数情况下,这是您想要的,您应该改用它。但请记住,键仍然是真正的字符串!
This has just a length and numeric index signature. In the rare cases that this is what you want, you should use it instead. But remember that the keys are still really strings!
consttupleLike:ArrayLike<string>={'0':'A','1':'B',length:2,};// OK
consttupleLike:ArrayLike<string>={'0':'A','1':'B',length:2,};// OK
明白数组是对象,所以它们的键是字符串,而不是数字。number作为索引签名是一个纯粹的 TypeScript 构造,旨在帮助捕获错误。
Understand that arrays are objects, so their keys are strings, not numbers. number as an index signature is a purely TypeScript construct which is designed to help catch bugs.
Prefer Array, tuple, or ArrayLike types to using number in an index signature yourself.
Here’s some code to print the triangular numbers (1, 1+2, 1+2+3, etc.):
functionprintTriangles(n:number){constnums=[];for(leti=0;i<n;i++){nums.push(i);console.log(arraySum(nums));}}
functionprintTriangles(n:number){constnums=[];for(leti=0;i<n;i++){nums.push(i);console.log(arraySum(nums));}}
这段代码看起来很简单。但是运行它时会发生以下情况:
This code looks straightforward. But here’s what happens when you run it:
>打印三角形(5) 0 1个 2个 3个 4个
> printTriangles(5) 0 1 2 3 4
问题是您对 做了一个假设arraySum,即它不会修改nums。但这是我的实现:
The problem is that you’ve made an assumption about arraySum, namely, that it doesn’t modify nums. But here’s my implementation:
functionarraySum(arr:number[]){letsum=0,num;while((num=arr.pop())!==undefined){sum+=num;}returnsum;}
functionarraySum(arr:number[]){letsum=0,num;while((num=arr.pop())!==undefined){sum+=num;}returnsum;}
此函数确实计算数组中数字的总和。但是它也有清空数组的副作用!TypeScript 对此很好,因为 JavaScript 数组是可变的。
This function does calculate the sum of the numbers in the array. But it also has the side effect of emptying the array! TypeScript is fine with this, because JavaScript arrays are mutable.
arraySum有一些不修改数组的保证会很好。这是readonly类型修饰符的作用:
It would be nice to have some assurances that arraySum does not modify the array. This is what the readonly type modifier does:
functionarraySum(arr:readonlynumber[]){letsum=0,num;while((num=arr.pop())!==undefined){// ~~~ 'pop' does not exist on type 'readonly number[]'sum+=num;}returnsum;}
functionarraySum(arr:readonlynumber[]){letsum=0,num;while((num=arr.pop())!==undefined){// ~~~ 'pop' does not exist on type 'readonly number[]'sum+=num;}returnsum;}
此错误消息值得深入研究。readonly number[]是一种typenumber[] ,它在几个方面不同于:
This error message is worth digging into. readonly number[] is a type, and it is distinct from number[] in a few ways:
您可以从它的元素中读取,但不能写入它们。
You can read from its elements, but you can’t write to them.
您可以读取它的length,但不能设置它(这会改变数组)。
You can read its length, but you can’t set it (which would mutate the array).
您不能调用pop或改变数组的其他方法。
You can’t call pop or other methods that mutate the array.
因为number[]严格来说比 更有能力readonly number[],所以它number[]是 的子类型readonly number[]。(很容易倒过来——记住第 7 条!)所以你可以将一个可变数组赋值给一个readonly数组,但反之则不行:
Because number[] is strictly more capable than readonly number[], it follows that number[] is a subtype of readonly number[]. (It’s easy to get this backwards—remember Item 7!) So you can assign a mutable array to a readonly array, but not vice versa:
consta:number[]=[1,2,3];constb:readonlynumber[]=a;constc:number[]=b;// ~ Type 'readonly number[]' is 'readonly' and cannot be// assigned to the mutable type 'number[]'
consta:number[]=[1,2,3];constb:readonlynumber[]=a;constc:number[]=b;// ~ Type 'readonly number[]' is 'readonly' and cannot be// assigned to the mutable type 'number[]'
这是有道理的:readonly如果你可以在没有类型断言的情况下摆脱它,修饰符就没有多大用处。
This makes sense: the readonly modifier wouldn’t be much use if you could get rid of it without even a type assertion.
当你声明一个参数时readonly,会发生一些事情:
When you declare a parameter readonly, a few things happen:
TypeScript 检查参数在函数体中没有发生变化。
TypeScript checks that the parameter isn’t mutated in the function body.
调用者确信您的函数不会改变参数。
Callers are assured that your function doesn’t mutate the parameter.
调用者可以将readonly数组传递给您的函数。
Callers may pass your function a readonly array.
在 JavaScript(和 TypeScript)中通常有一个假设,即函数不会改变它们的参数,除非明确指出。但是正如我们将在本书中一次又一次地看到的(特别是第30条和第 31条),这些隐含的理解可能会导致类型检查出现问题。最好让它们明确,无论是对于人类读者还是对于tsc.
There is often an assumption in JavaScript (and TypeScript) that functions don’t mutate their parameters unless explicitly noted. But as we’ll see time and again in this book (particularly Items 30 and 31), these sorts of implicit understandings can lead to trouble with type checking. Better to make them explicit, both for human readers and for tsc.
解决方法arraySum很简单:不要改变数组!
The fix for arraySum is simple: don’t mutate the array!
functionarraySum(arr:readonlynumber[]){letsum=0;for(constnumofarr){sum+=num;}returnsum;}
functionarraySum(arr:readonlynumber[]){letsum=0;for(constnumofarr){sum+=num;}returnsum;}
现在printTriangles做你期望的:
Now printTriangles does what you expect:
>打印三角形(5) 0 1个 3个 6个 10
> printTriangles(5) 0 1 3 6 10
如果你的函数不改变它的参数,那么你应该声明它们readonly。缺点相对较小:用户将能够使用更广泛的类型集(Item 29)来调用它们,并且会发现无意的突变。
If your function does not mutate its parameters, then you should declare them readonly. There’s relatively little downside: users will be able to call them with a broader set of types (Item 29), and inadvertent mutations will be caught.
一个缺点是您可能需要调用未标记其参数的函数readonly。如果这些不改变它们的参数并且在您的控制之下,请制作它们readonly!readonly往往具有传染性:一旦用 标记了一个函数readonly,您还需要标记它调用的所有函数。这是一件好事,因为它导致更清晰的合同和更好的类型安全。但是,如果您正在调用另一个库中的函数,您可能无法更改其类型声明,并且您可能不得不求助于类型断言 ( param as number[])。
One downside is that you may need to call functions that haven’t marked their parameters readonly. If these don’t mutate their parameters and are in your control, make them readonly! readonly tends to be contagious: once you mark one function with readonly, you’ll also need to mark all the functions that it calls. This is a good thing since it leads to clearer contracts and better type safety. But if you’re calling a function in another library, you may not be able to change its type declarations, and you may have to resort to a type assertion (param as number[]).
readonly也可以用来捕捉一整类涉及局部变量的突变错误。假设您正在编写一个工具来处理小说。你获取一系列行并希望将它们收集到段落中,段落之间用空格分隔:
readonly can also be used to catch a whole class of mutation errors involving local variables. Imagine you’re writing a tool to process a novel. You get a sequence of lines and would like to collect them into paragraphs, which are separated by blanks:
弗兰肯斯坦;或者,现代普罗米修斯 通过玛丽·雪莱 你会很高兴听到没有灾难伴随着毕业典礼 一个你曾怀着如此邪恶的预感看待的企业。我到了 昨天在这里,我的首要任务是向我亲爱的姐姐保证我的幸福和 增加对我事业成功的信心。 我已经在伦敦北部很远的地方,当我走在彼得堡的街道上时, 我感到一阵寒冷的北风吹在我的脸颊上,使我的神经绷紧, 让我充满喜悦。
Frankenstein; or, The Modern Prometheus by Mary Shelley You will rejoice to hear that no disaster has accompanied the commencement of an enterprise which you have regarded with such evil forebodings. I arrived here yesterday, and my first task is to assure my dear sister of my welfare and increasing confidence in the success of my undertaking. I am already far north of London, and as I walk in the streets of Petersburgh, I feel a cold northern breeze play upon my cheeks, which braces my nerves and fills me with delight.
这是一个尝试:1
Here’s an attempt:1
functionparseTaggedText(lines:string[]):string[][]{constparagraphs:string[][]=[];constcurrPara:string[]=[];constaddParagraph=()=>{if(currPara.length){paragraphs.push(currPara);currPara.length=0;// Clear the lines}};for(constlineoflines){if(!line){addParagraph();}else{currPara.push(line);}}addParagraph();returnparagraphs;}
functionparseTaggedText(lines:string[]):string[][]{constparagraphs:string[][]=[];constcurrPara:string[]=[];constaddParagraph=()=>{if(currPara.length){paragraphs.push(currPara);currPara.length=0;// Clear the lines}};for(constlineoflines){if(!line){addParagraph();}else{currPara.push(line);}}addParagraph();returnparagraphs;}
当您在项目开头的示例中运行它时,您将得到以下结果:
When you run this on the example at the beginning of the item, here’s what you get:
[[],[],[]]
[[],[],[]]
好吧,那真是大错特错了!
Well that went horribly wrong!
这段代码的问题是别名(第 24 项)和变异的有害组合。别名发生在这一行:
The problem with this code is a toxic combination of aliasing (Item 24) and mutation. The aliasing happens on this line:
paragraphs.push(currPara);
paragraphs.push(currPara);
这不是推送 的内容currPara,而是推送对数组的引用。当您将新值推入currPara或清除它时,此更改也会反映在中的条目中,paragraphs因为它们指向同一对象。
Rather than pushing the contents of currPara, this pushes a reference to the array. When you push a new value to currPara or clear it, this change is also reflected in the entries in paragraphs because they point to the same object.
换句话说,这段代码的净效果:
In other words, the net effect of this code:
paragraphs.push(currPara);currPara.length=0;// Clear lines
paragraphs.push(currPara);currPara.length=0;// Clear lines
是你将一个新段落推到paragraphs然后立即清除它。
is that you push a new paragraph onto paragraphs and then immediately clear it.
问题是设置currPara.length和调用currPara.push都会改变currPara数组。您可以通过将其声明为 来禁止此行为readonly。这立即显示出实施中的一些错误:
The problem is that setting currPara.length and calling currPara.push both mutate the currPara array. You can disallow this behavior by declaring it to be readonly. This immediately surfaces a few errors in the implementation:
functionparseTaggedText(lines:string[]):string[][]{constcurrPara:readonlystring[]=[];constparagraphs:string[][]=[];constaddParagraph=()=>{if(currPara.length){paragraphs.push(currPara// ~~~~~~~~ Type 'readonly string[]' is 'readonly' and// cannot be assigned to the mutable type 'string[]');currPara.length=0;// Clear lines// ~~~~~~ Cannot assign to 'length' because it is a read-only// property}};for(constlineoflines){if(!line){addParagraph();}else{currPara.push(line);// ~~~~ Property 'push' does not exist on type 'readonly string[]'}}addParagraph();returnparagraphs;}
functionparseTaggedText(lines:string[]):string[][]{constcurrPara:readonlystring[]=[];constparagraphs:string[][]=[];constaddParagraph=()=>{if(currPara.length){paragraphs.push(currPara// ~~~~~~~~ Type 'readonly string[]' is 'readonly' and// cannot be assigned to the mutable type 'string[]');currPara.length=0;// Clear lines// ~~~~~~ Cannot assign to 'length' because it is a read-only// property}};for(constlineoflines){if(!line){addParagraph();}else{currPara.push(line);// ~~~~ Property 'push' does not exist on type 'readonly string[]'}}addParagraph();returnparagraphs;}
currPara您可以通过声明和使用非变异方法来修复其中两个错误let:
You can fix two of the errors by declaring currPara with let and using nonmutating methods:
letcurrPara:readonlystring[]=[];// ...currPara=[];// Clear lines// ...currPara=currPara.concat([line]);
letcurrPara:readonlystring[]=[];// ...currPara=[];// Clear lines// ...currPara=currPara.concat([line]);
与 不同push,concat返回一个新数组,原始数组保持不变。通过将声明从 更改为const并let添加readonly,您已经将一种可变性换成了另一种。该currPara变量现在可以自由更改它指向的数组,但不允许更改这些数组本身。
Unlike push, concat returns a new array, leaving the original unmodified. By changing the declaration from const to let and adding readonly, you’ve traded one sort of mutability for another. The currPara variable is now free to change which array it points to, but those arrays themselves are not allowed to change.
这留下了关于 的错误paragraphs。您可以通过三个选项来解决此问题。
This leaves the error about paragraphs. You have three options for fixing this.
首先,您可以复制currPara:
First, you could make a copy of currPara:
paragraphs.push([...currPara]);
paragraphs.push([...currPara]);
这修复了错误,因为在currPara保留的同时readonly,您可以随意更改副本。
This fixes the error because, while currPara remains readonly, you’re free to mutate the copy however you like.
其次,您可以将paragraphs(以及函数的返回类型)更改为一个数组readonly string[]:
Second, you could change paragraphs (and the return type of the function) to be an array of readonly string[]:
constparagraphs:(readonlystring[])[]=[];
constparagraphs:(readonlystring[])[]=[];
(这里的分组是相关的:readonly string[][]将是readonly可变数组的数组,而不是可变数组的readonly数组。)
(The grouping is relevant here: readonly string[][] would be a readonly array of mutable arrays, rather than a mutable array of readonly arrays.)
这行得通,但对parseTaggedText. 你为什么关心函数返回后他们对段落做了什么?
This works, but it seems a bit rude to users of parseTaggedText. Why do you care what they do with the paragraphs after the function returns?
第三,您可以使用断言来删除readonly数组的 -ness :
Third, you could use an assertion to remove the readonly-ness of the array:
paragraphs.push(currParaasstring[]);
paragraphs.push(currParaasstring[]);
由于您要currPara在下一个语句中分配给一个新数组,所以这似乎不是最令人反感的断言。
Since you’re assigning currPara to a new array in the very next statement, this doesn’t seem like the most offensive assertion.
一个重要的警告readonly是它很浅。你readonly string[][]之前看到过这个。如果你有一个readonly对象数组,对象本身不是readonly:
An important caveat to readonly is that it is shallow. You saw this with readonly string[][] earlier. If you have a readonly array of objects, the objects themselves are not readonly:
constdates:readonlyDate[]=[newDate()];dates.push(newDate());// ~~~~ Property 'push' does not exist on type 'readonly Date[]'dates[0].setFullYear(2037);// OK
constdates:readonlyDate[]=[newDate()];dates.push(newDate());// ~~~~ Property 'push' does not exist on type 'readonly Date[]'dates[0].setFullYear(2037);// OK
类似的考虑适用于readonly对象的表亲,Readonly通用的:
Similar considerations apply to readonly’s cousin for objects, the Readonly generic:
interfaceOuter{inner:{x::::number;}}consto_Readonly<Outer>={inner:{x_0}};o.inner={x_1};// ~~~~ Cannot assign to 'inner' because it is a read-only propertyo.inner.x=1;// OK
interfaceOuter{inner:{x:number;}}consto:Readonly<Outer>={inner:{x:0}};o.inner={x:1};// ~~~~ Cannot assign to 'inner' because it is a read-only propertyo.inner.x=1;// OK
您可以创建一个类型别名,然后在您的编辑器中检查它以查看到底发生了什么:
You can create a type alias and then inspect it in your editor to see exactly what’s happening:
typeT=Readonly<Outer>;// Type T = {// readonly inner: {// x: number;// };// }
typeT=Readonly<Outer>;// Type T = {// readonly inner: {// x: number;// };// }
需要注意的重要一点是readonly修饰符 oninner而不是 on x。在撰写本文时,还没有对深度只读类型的内置支持,但可以创建一个泛型来执行此操作。做到这一点很棘手,所以我建议使用库而不是自己动手。泛型DeepReadonly是ts-essentials一个实现。
The important thing to note is the readonly modifier on inner but not on x. There is no built-in support for deep readonly types at the time of this writing, but it is possible to create a generic to do this. Getting this right is tricky, so I recommend using a library rather than rolling your own. The DeepReadonly generic in ts-essentials is one implementation.
你也可以写readonly在索引签名上。这具有防止写入但允许读取的效果:
You can also write readonly on an index signature. This has the effect of preventing writes but allowing reads:
letobj:{readonly[k:string]:number}={};// Or Readonly<{[k: string]: number}obj.hi=45;// ~~ Index signature in type ... only permits readingobj={...obj,hi:12};// OKobj={...obj,bye:34};// OK
letobj:{readonly[k:string]:number}={};// Or Readonly<{[k: string]: number}obj.hi=45;// ~~ Index signature in type ... only permits readingobj={...obj,hi:12};// OKobj={...obj,bye:34};// OK
这可以防止涉及对象而不是数组的别名和变异问题。
This can prevent issues with aliasing and mutation involving objects rather than arrays.
如果您的函数不修改其参数,则声明它们readonly。这使得它的合同更加清晰,并防止在其实施过程中出现无意的变化。
If your function does not modify its parameters then declare them readonly. This makes its contract clearer and prevents inadvertent mutations in its implementation.
用于readonly防止突变错误并查找代码中发生突变的位置。
Use readonly to prevent errors with mutation and to find the places in your code where mutations occur.
const了解和之间的区别readonly。
Understand the difference between const and readonly.
认为您正在编写用于绘制散点图的 UI 组件。它有几种不同类型的属性来控制其显示和行为:
Suppose you’re writing a UI component for drawing scatter plots. It has a few different types of properties that control its display and behavior:
interfaceScatterProps{// The dataxs:number[];ys:number[];// DisplayxRange:[number,number];yRange:[number,number];color:string;// EventsonClick:(x:number,y:number,index:number)=>void;}
interfaceScatterProps{// The dataxs:number[];ys:number[];// DisplayxRange:[number,number];yRange:[number,number];color:string;// EventsonClick:(x:number,y:number,index:number)=>void;}
为避免不必要的工作,您希望仅在需要时重绘图表。更改数据或显示属性将需要重绘,但更改事件处理程序则不需要。这某种优化在 React 组件中很常见,其中事件处理程序 Prop 可能会在每次渲染时设置为新的箭头函数。2个
To avoid unnecessary work, you’d like to redraw the chart only when you need to. Changing data or display properties will require a redraw, but changing the event handler will not. This sort of optimization is common in React components, where an event handler Prop might be set to a new arrow function on every render.2
以下是您可以实现此优化的一种方式:
Here’s one way you might implement this optimization:
functionshouldUpdate(oldProps:ScatterProps,newProps:ScatterProps){letk:keyofScatterProps;for(kinoldProps){if(oldProps[k]!==newProps[k]){if(k!=='onClick')returntrue;}}returnfalse;}
functionshouldUpdate(oldProps:ScatterProps,newProps:ScatterProps){letk:keyofScatterProps;for(kinoldProps){if(oldProps[k]!==newProps[k]){if(k!=='onClick')returntrue;}}returnfalse;}
(有关此循环中的声明的解释,请参阅条目 54。keyof)
(See Item 54 for an explanation of the keyof declaration in this loop.)
当您或同事添加新属性时会发生什么?该shouldUpdate函数将在图表发生变化时重新绘制图表。您可以将此称为保守或“故障关闭”方法。好处是图表总是看起来正确。缺点是它可能会被绘制得太频繁。
What happens when you or a coworker add a new property? The shouldUpdate function will redraw the chart whenever it changes. You might call this the conservative or “fail closed” approach. The upside is that the chart will always look right. The downside is that it might be drawn too often.
“故障打开”方法可能如下所示:
A “fail open” approach might look like this:
functionshouldUpdate(oldProps:ScatterProps,newProps:ScatterProps){return(oldProps.xs!==newProps.xs||oldProps.ys!==newProps.ys||oldProps.xRange!==newProps.xRange||oldProps.yRange!==newProps.yRange||oldProps.color!==newProps.color// (no check for onClick));}
functionshouldUpdate(oldProps:ScatterProps,newProps:ScatterProps){return(oldProps.xs!==newProps.xs||oldProps.ys!==newProps.ys||oldProps.xRange!==newProps.xRange||oldProps.yRange!==newProps.yRange||oldProps.color!==newProps.color// (no check for onClick));}
使用这种方法不会有任何不必要的重绘,但可能会丢弃一些必要的绘制。这违反了优化的“先行无害”原则,因此不太常见。
With this approach there won’t be any unnecessary redraws, but there might be some necessary draws that get dropped. This violates the “first, do no harm” principle of optimization and so is less common.
这两种方法都不是理想的。您真正想要的是在添加新属性时强迫您的同事或未来的自己做出决定。您可以尝试添加评论:
Neither approach is ideal. What you’d really like is to force your coworker or future self to make a decision when adding the new property. You might try adding a comment:
interfaceScatterProps{xs::::::number[];ys_number[];// ...onClick:(x_number,y_number,index_number)=>void;// Note: if you add a property here, update shouldUpdate!}
interfaceScatterProps{xs:number[];ys:number[];// ...onClick:(x:number,y:number,index:number)=>void;// Note: if you add a property here, update shouldUpdate!}
但你真的希望这能奏效吗?如果类型检查器可以为您强制执行此操作,那就更好了。
But do you really expect this to work? It would be better if the type checker could enforce this for you.
如果您以正确的方式设置它,它可以。关键是使用映射类型和对象:
If you set it up the right way, it can. The key is to use a mapped type and an object:
constREQUIRES_UPDATE:{[kinkeyofScatterProps]:boolean}={xs:::::::true,ys_true,xRange_true,yRange_true,color_true,onClick_false,};functionshouldUpdate(oldProps:ScatterProps,newProps:ScatterProps){letk:keyofScatterProps;for(kinoldProps){if(oldProps[k]!==newProps[k]&&REQUIRES_UPDATE[k]){returntrue;}}returnfalse;}
constREQUIRES_UPDATE:{[kinkeyofScatterProps]:boolean}={xs:true,ys:true,xRange:true,yRange:true,color:true,onClick:false,};functionshouldUpdate(oldProps:ScatterProps,newProps:ScatterProps){letk:keyofScatterProps;for(kinoldProps){if(oldProps[k]!==newProps[k]&&REQUIRES_UPDATE[k]){returntrue;}}returnfalse;}
告诉[k in keyof ScatterProps]类型检查器REQUIRES_UPDATES应该具有与ScatterProps. 如果将来您将新属性添加到ScatterProps:
The [k in keyof ScatterProps] tells the type checker that REQUIRES_UPDATES should have all the same properties as ScatterProps. If future you adds a new property to ScatterProps:
interfaceScatterProps{// ...onDoubleClick:()=>void;}
interfaceScatterProps{// ...onDoubleClick:()=>void;}
那么这将在 的定义中产生错误REQUIRES_UPDATE:
Then this will produce an error in the definition of REQUIRES_UPDATE:
constREQUIRES_UPDATE:{[kinkeyofScatterProps]:boolean}={// ~~~~~~~~~~~~~~~ Property 'onDoubleClick' is missing in type// ...};
constREQUIRES_UPDATE:{[kinkeyofScatterProps]:boolean}={// ~~~~~~~~~~~~~~~ Property 'onDoubleClick' is missing in type// ...};
这肯定是强制的问题!删除或重命名属性将导致类似的错误。
This will certainly force the issue! Deleting or renaming a property will cause a similar error.
重要的是我们在这里使用了一个带有布尔值的对象。如果我们使用数组:
It’s important that we used an object with boolean values here. Had we used an array:
constPROPS_REQUIRING_UPDATE:(keyofScatterProps)[]=['xs','ys',// ...];
constPROPS_REQUIRING_UPDATE:(keyofScatterProps)[]=['xs','ys',// ...];
那么我们将被迫进入相同的失败打开/失败关闭选择。
then we would have been forced into the same fail open/fail closed choice.
如果您希望一个对象具有与另一个对象完全相同的属性,则映射类型是理想的选择。如本例所示,您可以使用它来让 TypeScript 对您的代码强制执行约束。
Mapped types are ideal if you want one object to have exactly the same properties as another. As in this example, you can use this to make TypeScript enforce constraints on your code.
为了工业中使用的编程语言,“静态类型”和“显式类型”有传统上是同义词。C、C++、Java:它们都让你写出你的类型。但学术语言从未将这两者混为一谈:像 ML 和 Haskell 这样的语言长期以来一直拥有复杂的类型推理系统,并且在过去十年中,这已经开始进入行业语言。C++ 添加了auto, 和Java 添加了var.
For programming languages used in industry, “statically typed” and “explicitly typed” have traditionally been synonymous. C, C++, Java: they all made you write out your types. But academic languages never conflated these two things: languages like ML and Haskell have long had sophisticated type inference systems, and in the past decade this has begun to work its way into industry languages. C++ has added auto, and Java has added var.
TypeScript 广泛使用类型推断。如果使用得当,这可以显着减少代码获得完全类型安全所需的类型注释的数量。区分 TypeScript 初学者和经验丰富的用户的最简单方法之一是通过类型注释的数量。经验丰富的 TypeScript 开发人员会使用相对较少的注解(但使用它们会产生很好的效果),而初学者可能会将他们的代码淹没在冗余的类型注解中。
TypeScript makes extensive use of type inference. Used well, this can dramatically reduce the number of type annotations your code requires to get full type safety. One of the easiest ways to tell a TypeScript beginner from a more experienced user is by the number of type annotations. An experienced TypeScript developer will use relatively few annotations (but use them to great effect), while a beginner may drown their code in redundant type annotations.
本章向您展示了类型推断可能出现的一些问题以及如何解决这些问题。阅读后,您应该很好地理解 TypeScript 如何推断类型,何时仍需要编写类型声明,以及何时编写类型声明是个好主意,即使可以推断类型。
This chapter shows you some of the problems that can arise with type inference and how to fix them. After reading it, you should have a good understanding of how TypeScript infers types, when you still need to write type declarations, and when it’s a good idea to write type declarations even when a type can be inferred.
这许多新的 TypeScript 开发人员在从 JavaScript 转换代码库时做的第一件事就是用类型注释填充它。毕竟,TypeScript 是关于类型的!但在 TypeScript 中,许多注解是不必要的。为所有变量声明类型会适得其反,并且被认为是糟糕的风格。
The first thing that many new TypeScript developers do when they convert a codebase from JavaScript is fill it with type annotations. TypeScript is about types, after all! But in TypeScript many annotations are unnecessary. Declaring types for all your variables is counterproductive and is considered poor style.
不要写:
Don’t write:
letx:number=12;
letx:number=12;
相反,只需写:
Instead, just write:
letx=12;
letx=12;
如果将鼠标悬停x在编辑器中,您会看到其类型已被推断为(如图 3-1number所示)。
If you mouse over x in your editor, you’ll see that its type has been inferred as number (as shown in Figure 3-1).
显式类型注释是多余的。写它只会增加噪音。如果不确定类型,可以在编辑器中进行检查。
The explicit type annotation is redundant. Writing it just adds noise. If you’re unsure of the type, you can check it in your editor.
TypeScript will also infer the types of more complex objects. Instead of:
constperson:{name::::::string;born:{where_string;when_string;};died:{where_string;when_string;}}={name:'Sojourner Truth',born:{where:'Swartekill, NY',when:'c.1797',},died:{where:'Battle Creek, MI',when:'Nov. 26, 1883'}};
constperson:{name:string;born:{where:string;when:string;};died:{where:string;when:string;}}={name:'Sojourner Truth',born:{where:'Swartekill, NY',when:'c.1797',},died:{where:'Battle Creek, MI',when:'Nov. 26, 1883'}};
你可以写:
you can just write:
constperson={name:'Sojourner Truth',born:{where:'Swartekill, NY',when:'c.1797',},died:{where:'Battle Creek, MI',when:'Nov. 26, 1883'}};
constperson={name:'Sojourner Truth',born:{where:'Swartekill, NY',when:'c.1797',},died:{where:'Battle Creek, MI',when:'Nov. 26, 1883'}};
同样,类型完全相同。除了值之外还写类型只会在这里增加噪音。(第 21 条对对象字面量的推断类型有更多说明。)
Again, the types are exactly the same. Writing the type in addition to the value just adds noise here. (Item 21 has more to say on the types inferred for object literals.)
适用于对象的情况也适用于数组。TypeScript 可以根据其输入和操作轻松确定此函数的返回类型:
What’s true for objects is also true for arrays. TypeScript has no trouble figuring out the return type of this function based on its inputs and operations:
functionsquare(nums:number[]){returnnums.map(x=>x*x);}constsquares=square([1,2,3,4]);// Type is number[]
functionsquare(nums:number[]){returnnums.map(x=>x*x);}constsquares=square([1,2,3,4]);// Type is number[]
TypeScript 可能会推断出比您预期的更精确的东西。这通常是一件好事。例如:
TypeScript may infer something more precise than what you expected. This is generally a good thing. For example:
constaxis1:string='x';// Type is stringconstaxis2='y';// Type is "y"
constaxis1:string='x';// Type is stringconstaxis2='y';// Type is "y"
"y"是更精确的变量类型axis。条目 21给出了一个如何修复类型错误的示例。
"y" is a more precise type for the axis variable. Item 21 gives an example of how this can fix a type error.
允许推断类型也可以促进重构。假设您有一个Product类型和一个函数来记录它:
Allowing types to be inferred can also facilitate refactoring. Say you have a Product type and a function to log it:
interfaceProduct{id:number;name:string;price:number;}functionlogProduct(product::::Product){constid_number=product.id;constname_string=product.name;constprice_number=product.price;console.log(id,name,price);}
interfaceProduct{id:number;name:string;price:number;}functionlogProduct(product:Product){constid:number=product.id;constname:string=product.name;constprice:number=product.price;console.log(id,name,price);}
在某些时候,您了解到产品 ID 中可能除了数字之外还包含字母。id所以你改变了in的类型Product。因为您在 中的所有变量上都包含了显式注释logProduct,所以会产生错误:
At some point you learn that product IDs might have letters in them in addition to numbers. So you change the type of id in Product. Because you included explicit annotations on all the variables in logProduct, this produces an error:
interfaceProduct{id:string;name:string;price:number;}functionlogProduct(product::::Product){constid_number=product.id;// ~~ Type 'string' is not assignable to type 'number'constname_string=product.name;constprice_number=product.price;console.log(id,name,price);}
interfaceProduct{id:string;name:string;price:number;}functionlogProduct(product:Product){constid:number=product.id;// ~~ Type 'string' is not assignable to type 'number'constname:string=product.name;constprice:number=product.price;console.log(id,name,price);}
如果您去掉了函数体中的所有注释logProduct,代码将无需修改就可以通过类型检查器。
Had you left off all the annotations in the logProduct function body, the code would have passed the type checker without modification.
更好的实现logProduct是使用解构赋值(条目 58):
A better implementation of logProduct would use destructuring assignment (Item 58):
functionlogProduct(product:Product){const{id,name,price}=product;console.log(id,name,price);}
functionlogProduct(product:Product){const{id,name,price}=product;console.log(id,name,price);}
此版本允许推断所有局部变量的类型。具有显式类型注释的相应版本重复且混乱:
This version allows the types of all the local variables to be inferred. The corresponding version with explicit type annotations is repetitive and cluttered:
functionlogProduct(product::::Product){const{id,name,price}:{id_string;name_string;price_number}=product;console.log(id,name,price);}
functionlogProduct(product:Product){const{id,name,price}:{id:string;name:string;price:number}=product;console.log(id,name,price);}
在 TypeScript 没有足够的上下文来自行确定类型的某些情况下,仍然需要显式类型注释。您之前已经看过其中之一:函数参数。
Explicit type annotations are still required in some situations where TypeScript doesn’t have enough context to determine a type on its own. You have seen one of these before: function parameters.
有些语言会根据参数的最终用途推断参数的类型,但 TypeScript 不会。在 TypeScript 中,变量的类型通常在首次引入时确定。
Some languages will infer types for parameters based on their eventual usage, but TypeScript does not. In TypeScript, a variable’s type is generally determined when it is first introduced.
理想的 TypeScript 代码包括函数/方法签名的类型注释,但不包括在其主体中创建的局部变量。这样可以将噪音降到最低,并让读者专注于实现逻辑。
Ideal TypeScript code includes type annotations for function/method signatures but not for the local variables created in their bodies. This keeps noise to a minimum and lets readers focus on the implementation logic.
在某些情况下,您也可以将类型注释从函数参数中移除。当有默认值时,例如:
There are some situations where you can leave the type annotations off of function parameters, too. When there’s a default value, for example:
functionparseNumber(str:string,base=10){// ...}
functionparseNumber(str:string,base=10){// ...}
这里的类型base被推断为number因为默认值10。
Here the type of base is inferred as number because of the default value of 10.
当函数用作具有类型声明的库的回调时,通常可以推断出参数类型。不需要在本例中使用 express HTTP 服务器库的request声明:response
Parameter types can usually be inferred when the function is used as a callback for a library with type declarations. The declarations on request and response in this example using the express HTTP server library are not required:
// Don't do this:app.get('/health',(request:express.Request,response:express.Response)=>{response.send('OK');});// Do this:app.get('/health',(request,response)=>{response.send('OK');});
// Don't do this:app.get('/health',(request:express.Request,response:express.Response)=>{response.send('OK');});// Do this:app.get('/health',(request,response)=>{response.send('OK');});
Item 26更深入地讨论了如何在类型推断中使用上下文。
Item 26 goes into more depth on how context is used in type inference.
在某些情况下,您可能仍然希望指定类型,即使它可以被推断出来。
There are a few situations where you may still want to specify a type even where it can be inferred.
一种是当你定义一个对象字面量时:
One is when you define an object literal:
constelmo:Product={name:'Tickle Me Elmo',id:'048188 627152',price:28.99,};
constelmo:Product={name:'Tickle Me Elmo',id:'048188 627152',price:28.99,};
当您在这样的定义上指定类型时,您启用了额外的属性检查(第 11 项)。这有助于捕获错误,特别是对于具有可选字段的类型。
When you specify a type on a definition like this, you enable excess property checking (Item 11). This can help catch errors, particularly for types with optional fields.
您还增加了在正确位置报告错误的几率。如果你离开注释,对象定义中的错误将导致使用它的地方出现类型错误,而不是它定义的地方:
You also increase the odds that an error will be reported in the right place. If you leave off the annotation, a mistake in the object’s definition will result in a type error where it’s used, rather than where it’s defined:
constfurby={name:'Furby',id:630509430963,price:35,};logProduct(furby);// ~~~~~ Argument .. is not assignable to parameter of type 'Product'// Types of property 'id' are incompatible// Type 'number' is not assignable to type 'string'
constfurby={name:'Furby',id:630509430963,price:35,};logProduct(furby);// ~~~~~ Argument .. is not assignable to parameter of type 'Product'// Types of property 'id' are incompatible// Type 'number' is not assignable to type 'string'
有了注解,就会在出错的地方得到更简洁的错误提示:
With an annotation, you get a more concise error in the place where the mistake was made:
constfurby:Product={name:'Furby',id:630509430963,// ~~ Type 'number' is not assignable to type 'string'price:35,};logProduct(furby);
constfurby:Product={name:'Furby',id:630509430963,// ~~ Type 'number' is not assignable to type 'string'price:35,};logProduct(furby);
类似的考虑适用于函数的返回类型。即使可以推断出它以确保实现错误不会泄漏到函数的使用中,您可能仍然希望对其进行注释。
Similar considerations apply to a function’s return type. You may still want to annotate this even when it can be inferred to ensure that implementation errors don’t leak out into uses of the function.
假设您有一个检索股票报价的函数:
Say you have a function which retrieves a stock quote:
functiongetQuote(ticker:string){returnfetch(`https://quotes.example.com/?q=${ticker}`).then(response=>response.json());}
functiongetQuote(ticker:string){returnfetch(`https://quotes.example.com/?q=${ticker}`).then(response=>response.json());}
您决定添加缓存以避免重复网络请求:
You decide to add a cache to avoid duplicating network requests:
constcache:{[ticker:string]:number}={};functiongetQuote(ticker:string){if(tickerincache){returncache[ticker];}returnfetch(`https://quotes.example.com/?q=${ticker}`).then(response=>response.json()).then(quote=>{cache[ticker]=quote;returnquote;});}
constcache:{[ticker:string]:number}={};functiongetQuote(ticker:string){if(tickerincache){returncache[ticker];}returnfetch(`https://quotes.example.com/?q=${ticker}`).then(response=>response.json()).then(quote=>{cache[ticker]=quote;returnquote;});}
这个实现中有一个错误:你真的应该返回Promise.resolve(cache[ticker]),这样getQuote总是返回一个 Promise。该错误很可能会产生错误……但在调用 的代码中getQuote,而不是在getQuote其本身中:
There’s a mistake in this implementation: you should really be returning Promise.resolve(cache[ticker]) so that getQuote always returns a Promise. The mistake will most likely produce an error…but in the code that calls getQuote, rather than in getQuote itself:
getQuote('MSFT').then(considerBuying);// ~~~~ Property 'then' does not exist on type// 'number | Promise<any>'// Property 'then' does not exist on type 'number'
getQuote('MSFT').then(considerBuying);// ~~~~ Property 'then' does not exist on type// 'number | Promise<any>'// Property 'then' does not exist on type 'number'
如果您注释了预期的返回类型 ( Promise<number>),错误将在正确的位置报告:
Had you annotated the intended return type (Promise<number>), the error would have been reported in the correct place:
constcache:{[ticker:string]:number}={};functiongetQuote(ticker:string):Promise<number>{if(tickerincache){returncache[ticker];// ~~~~~~~~~~~~~ Type 'number' is not assignable to 'Promise<number>'}// ...}
constcache:{[ticker:string]:number}={};functiongetQuote(ticker:string):Promise<number>{if(tickerincache){returncache[ticker];// ~~~~~~~~~~~~~ Type 'number' is not assignable to 'Promise<number>'}// ...}
当您注释返回类型时,它可以防止实现错误在用户代码中显示为错误。(有关异步函数的讨论,请参阅第 25 项,这是避免 Promises 出现此特定错误的有效方法。)
When you annotate the return type, it keeps implementation errors from manifesting as errors in user code. (See Item 25 for a discussion of async functions, which are an effective way to avoid this specific error with Promises.)
写出返回类型也可以帮助你更清楚地思考你的函数:在你实现它之前你应该知道它的输入和输出类型是什么。虽然实现可能会稍微改变,但函数的契约(它的类型签名)通常不应该。这在精神上与测试驱动开发 (TDD) 类似,在测试驱动开发中,您在实现功能之前编写测试来执行它。首先编写完整的类型签名有助于获得所需的功能,而不是实现权宜之计的功能。
Writing out the return type may also help you think more clearly about your function: you should know what its input and output types are before you implement it. While the implementation may shift around a bit, the function’s contract (its type signature) generally should not. This is similar in spirit to test-driven development (TDD), in which you write the tests that exercise a function before you implement it. Writing the full type signature first helps get you the function you want, rather than the one the implementation makes expedient.
注释返回值的最后一个原因是如果您想使用命名类型。您可以选择不为此函数编写返回类型,例如:
A final reason to annotate return values is if you want to use a named type. You might choose not to write a return type for this function, for example:
interfaceVector2D{x:::::::number;y_number;}functionadd(a_Vector2D,b_Vector2D){return{x_a.x+b.x,y_a.y+b.y};}
interfaceVector2D{x:number;y:number;}functionadd(a:Vector2D,b:Vector2D){return{x:a.x+b.x,y:a.y+b.y};}
TypeScript 将返回类型推断为{ x: number; y: number; }. 这与 兼容,但当您的代码用户将其视为输入类型而不是输出类型Vector2D时,他们可能会感到惊讶(如图3-2所示)。Vector2D
TypeScript infers the return type as { x: number; y: number; }. This is compatible with Vector2D, but it may be surprising to users of your code when they see Vector2D as a type of the input and not of the output (as shown in Figure 3-2).
如果您注释返回类型,则表示会更直接。如果您已经编写了关于该类型的文档(第 48 项),那么它也将与返回值相关联。随着推断返回类型的复杂性增加,提供名称变得越来越有用。
If you annotate the return type, the presentation is more straightforward. And if you’ve written documentation on the type (Item 48) then it will be associated with the returned value as well. As the complexity of the inferred return type increases, it becomes increasingly helpful to provide a name.
如果您使用的是 linter,则eslint 规则no-inferrable-types(注意变体拼写)可以帮助确保所有类型注释都是真正必要的。
If you are using a linter, the eslint rule no-inferrable-types (note the variant spelling) can help ensure that all your type annotations are really necessary.
当 TypeScript 可以推断相同类型时,避免编写类型注释。
Avoid writing type annotations when TypeScript can infer the same type.
理想情况下,您的代码在函数/方法签名中有类型注释,但在它们的主体中没有局部变量。
Ideally your code has type annotations in function/method signatures but not on local variables in their bodies.
Consider using explicit annotations for object literals and function return types even when they can be inferred. This will help prevent implementation errors from surfacing in user code.
在JavaScript 可以为了不同的目的重用一个变量来保存不同类型的值:
In JavaScript it’s no problem to reuse a variable to hold a differently typed value for a different purpose:
letid="12-34-56";fetchProduct(id);// Expects a stringid=123456;fetchProductBySerialNumber(id);// Expects a number
letid="12-34-56";fetchProduct(id);// Expects a stringid=123456;fetchProductBySerialNumber(id);// Expects a number
在 TypeScript 中,这会导致两个错误:
In TypeScript, this results in two errors:
letid="12-34-56";fetchProduct(id);id=123456;// ~~ '123456' is not assignable to type 'string'.fetchProductBySerialNumber(id);// ~~ Argument of type 'string' is not assignable to// parameter of type 'number'
letid="12-34-56";fetchProduct(id);id=123456;// ~~ '123456' is not assignable to type 'string'.fetchProductBySerialNumber(id);// ~~ Argument of type 'string' is not assignable to// parameter of type 'number'
将鼠标悬停在编辑器中的第一个上id可以提示正在发生的事情(参见图 3-3)。
Hovering over the first id in your editor gives a hint as to what’s going on (see Figure 3-3).
根据该值,TypeScript 将其类型"12-34-56"推断为。您不能将 a 分配给 a ,因此无法分配错误。idstringnumberstring
Based on the value "12-34-56", TypeScript has inferred id’s type as string. You can’t assign a number to a string and hence the error.
这让我们对 TypeScript 中的变量有了一个关键的认识:虽然变量的值可以改变,但它的类型通常不会。一个类型可以改变的一种常见方式是缩小(条目 22),但这涉及到一个类型变得更小,而不是扩展以包含新值。此规则(第 41 项)有一些重要的例外情况,但它们是例外情况而不是规则。
This leads us to a key insight about variables in TypeScript: while a variable’s value can change, its type generally does not. The one common way a type can change is to narrow (Item 22), but this involves a type getting smaller, not expanding to include new values. There are some important exceptions to this rule (Item 41), but they are the exceptions and not the rule.
你如何使用这个想法来修复这个例子?为了id不改变 ' 的类型,它必须足够广泛以包含strings 和numbers。这是联合类型的定义,string|number:
How can you use this idea to fix the example? In order for id’s type to not change, it must be broad enough to encompass both strings and numbers. This is the very definition of the union type, string|number:
letid:string|number="12-34-56";fetchProduct(id);id=123456;// OKfetchProductBySerialNumber(id);// OK
letid:string|number="12-34-56";fetchProduct(id);id=123456;// OKfetchProductBySerialNumber(id);// OK
这修复了错误。有趣的是,TypeScript 能够在第一次调用中确定它id确实是 a ,而在第二次调用中确实是 a。它根据分配缩小了联合类型。stringnumber
This fixes the errors. It’s interesting that TypeScript has been able to determine that id is really a string in the first call and really a number in the second. It has narrowed the union type based on the assignment.
虽然联合类型确实有效,但它可能会在未来产生更多问题。string联合类型比or这样的简单类型更难使用,number因为在对它们进行任何操作之前,您通常必须检查它们是什么。
While a union type does work, it may create more issues down the road. Union types are harder to work with than simple types like string or number because you usually have to check what they are before you do anything with them.
更好的解决方案是引入一个新变量:
The better solution is to introduce a new variable:
constid="12-34-56";fetchProduct(id);constserial=123456;// OKfetchProductBySerialNumber(serial);// OK
constid="12-34-56";fetchProduct(id);constserial=123456;// OKfetchProductBySerialNumber(serial);// OK
在以前的版本中,第一个和第二个id在语义上彼此不相关。它们只是因为您重用了一个变量而相关。这会让类型检查器感到困惑,也会让人类读者感到困惑。
In the previous version, the first and second id were not semantically related to one another. They were only related by the fact that you reused a variable. This was confusing for the type checker and would be confusing for a human reader, too.
出于多种原因,具有两个变量的版本更好:
The version with two variables is better for a number of reasons:
它理清了两个不相关的概念(ID 和序列号)。
It disentangles two unrelated concepts (ID and serial number).
它允许您使用更具体的变量名称。
It allows you to use more specific variable names.
它改进了类型推断。不需要类型注释。
It improves type inference. No type annotations are needed.
它导致更简单的类型(stringand number,而不是string|number)。
It results in simpler types (string and number, rather than string|number).
它允许您声明变量const而不是let. 这使人们和类型检查器更容易推理它们。
It lets you declare the variables const rather than let. This makes them easier for people and the type checker to reason about.
尽量避免改变类型的变量。如果您可以为不同的概念使用不同的名称,那么您的代码对于人类读者和类型检查器来说都会更加清晰。
Try to avoid type-changing variables. If you can use different names for different concepts, it will make your code clearer both to human readers and to the type checker.
这不要与本例中的“阴影”变量混淆:
This is not to be confused with “shadowed” variables as in this example:
constid="12-34-56";fetchProduct(id);{constid=123456;// OKfetchProductBySerialNumber(id);// OK}
constid="12-34-56";fetchProduct(id);{constid=123456;// OKfetchProductBySerialNumber(id);// OK}
虽然这两个ids 共享一个名称,但它们实际上是两个不同的变量,彼此之间没有任何关系。他们有不同的类型很好。虽然 TypeScript 不会对此感到困惑,但您的人类读者可能会。一般来说,不同的概念最好使用不同的名称。许多团队选择通过 linter 规则禁止这种阴影。
While these two ids share a name, they are actually two distinct variables with no relationship to one another. It’s fine for them to have different types. While TypeScript is not confused by this, your human readers might be. In general it’s better to use different names for different concepts. Many teams choose to disallow this sort of shadowing via linter rules.
此项目侧重于标量值,但类似的考虑也适用于对象。有关更多信息,请参阅条目 23。
This item focused on scalar values, but similar considerations apply to objects. For more on that, see Item 23.
作为 Item 7解释说,在运行时每个变量都有一个值。但是在静态分析时,当 TypeScript 检查你的代码时,一个变量有一组可能的值,即它的类型。当您使用常量初始化变量但未提供类型时,类型检查器需要确定一个。换句话说,它需要根据您指定的单个值来决定一组可能的值。在 TypeScript 中,这个过程被称为widening。理解它将帮助您理解错误并更有效地使用类型注释。
As Item 7 explained, at runtime every variable has a single value. But at static analysis time, when TypeScript is checking your code, a variable has a set of possible values, namely, its type. When you initialize a variable with a constant but don’t provide a type, the type checker needs to decide on one. In other words, it needs to decide on a set of possible values from the single value that you specified. In TypeScript, this process is known as widening. Understanding it will help you make sense of errors and make more effective use of type annotations.
假设您正在编写一个库来处理向量。你写出一个 3D 向量的类型和一个函数来获取它的任何组件的值:
Suppose you’re writing a library to work with vectors. You write out a type for a 3D vector and a function to get the value of any of its components:
interfaceVector3{x::::number;y_number;z_number;}functiongetComponent(vector_Vector3,axis:'x'|'y'|'z'){returnvector[axis];}
interfaceVector3{x:number;y:number;z:number;}functiongetComponent(vector:Vector3,axis:'x'|'y'|'z'){returnvector[axis];}
但是当你尝试使用它时,TypeScript 会标记一个错误:
But when you try to use it, TypeScript flags an error:
letx='x';letvec={x:10,y:20,z:30};getComponent(vec,x);// ~ Argument of type 'string' is not assignable to// parameter of type '"x" | "y" | "z"'
letx='x';letvec={x:10,y:20,z:30};getComponent(vec,x);// ~ Argument of type 'string' is not assignable to// parameter of type '"x" | "y" | "z"'
这段代码运行正常,为什么会报错呢?
This code runs fine, so why the error?
问题是 的x类型被推断为string,而getComponent函数期望其第二个参数的类型更具体。这在工作中正在扩大,在这里它导致了一个错误。
The issue is that x’s type is inferred as string, whereas the getComponent function expected a more specific type for its second argument. This is widening at work, and here it has led to an error.
这process 是不明确的,因为任何给定值都有许多可能的类型。在此声明中,例如:
This process is ambiguous in the sense that there are many possible types for any given value. In this statement, for example:
constmixed=['x',1];
constmixed=['x',1];
的类型应该mixed是什么?这里有几种可能性:
what should the type of mixed be? Here are a few possibilities:
('x' | 1)[]
('x' | 1)[]
['x', 1]
['x', 1]
[string, number]
[string, number]
readonly [string, number]
readonly [string, number]
(string|number)[]
(string|number)[]
readonly (string|number)[]
readonly (string|number)[]
[any, any]
[any, any]
any[]
any[]
没有更多上下文,TypeScript 无法知道哪个是“正确的”。它必须猜测你的意图。(在这种情况下,它会猜测(string|number)[]。)尽管它很聪明,但 TypeScript 无法读懂你的想法。它不会在 100% 的时间里做到这一点。结果是像我们刚刚看到的那样的无意错误。
Without more context, TypeScript has no way to know which one is “right.” It has to guess at your intent. (In this case, it guesses (string|number)[].) And smart as it is, TypeScript can’t read your mind. It won’t get this right 100% of the time. The result is inadvertent errors like the one we just looked at.
在最初的示例中,推断的类型x是string因为 TypeScript 选择允许这样的代码:
In the initial example, the type of x is inferred as string because TypeScript chooses to allow code like this:
letx='x';x='a';x='Four score and seven years ago...';
letx='x';x='a';x='Four score and seven years ago...';
但它也可以是有效的 JavaScript 来编写:
But it would also be valid JavaScript to write:
letx='x';x=/x|y|z/;x=['x','y','z'];
letx='x';x=/x|y|z/;x=['x','y','z'];
x在推断as的类型时string,TypeScript 试图在特异性和灵活性之间取得平衡。一般规则是变量的类型在声明后不应更改(第 20 项),因此比orstring更有意义。string|RegExpstring|string[]any
In inferring the type of x as string, TypeScript attempts to strike a balance between specificity and flexibility. The general rule is that a variable’s type shouldn’t change after it’s declared (Item 20), so string makes more sense than string|RegExp or string|string[] or any.
TypeScript 为您提供了几种控制加宽过程的方法。一个是const。const如果你用而不是声明一个变量let,它会得到一个更窄的类型。事实上,使用const修复了我们原始示例中的错误:
TypeScript gives you a few ways to control the process of widening. One is const. If you declare a variable with const instead of let, it gets a narrower type. In fact, using const fixes the error in our original example:
constx='x';// type is "x"letvec={x:10,y:20,z:30};getComponent(vec,x);// OK
constx='x';// type is "x"letvec={x:10,y:20,z:30};getComponent(vec,x);// OK
由于x无法重新分配,TypeScript 能够推断出更窄的类型,而不会无意中标记后续分配错误的风险。并且由于字符串文字类型"x"可分配给"x"|"y"|"z",因此代码通过了类型检查器。
Because x cannot be reassigned, TypeScript is able to infer a narrower type without risk of inadvertently flagging errors on subsequent assignments. And because the string literal type "x" is assignable to "x"|"y"|"z", the code passes the type checker.
const然而,这不是万灵药。对于对象和数组,仍然存在歧义。这里的例子mixed说明了数组的问题:TypeScript 应该推断元组类型吗?它应该为元素推断出什么类型?对象也会出现类似的问题。这段代码在 JavaScript 中没有问题:
const isn’t a panacea, however. For objects and arrays, there is still ambiguity. The mixed example here illustrates the issue for arrays: should TypeScript infer a tuple type? What type should it infer for the elements? Similar issues arise with objects. This code is fine in JavaScript:
constv={x:1,};v.x=3;v.x='3';v.y=4;v.name='Pythagoras';
constv={x:1,};v.x=3;v.x='3';v.y=4;v.name='Pythagoras';
的类型v可以在特异性范围内的任何地方推断出来。在特定的一端是{readonly x: 1}. 更一般的是{x: number}。更一般的还是{[key: string]: number}or object。对于对象,TypeScript 的扩展算法将每个元素视为分配了let. 所以类型v为{x: number}。这使您可以重新分配v.x给不同的号码,但不能重新分配给string. 它会阻止您添加其他属性。(这是一次性构建所有对象的一个很好的理由,如条款 23中所述。)
The type of v could be inferred anywhere along the spectrum of specificity. At the specific end is {readonly x: 1}. More general is {x: number}. More general still would be {[key: string]: number} or object. In the case of objects, TypeScript’s widening algorithm treats each element as though it were assigned with let. So the type of v comes out as {x: number}. This lets you reassign v.x to a different number, but not to a string. And it prevents you from adding other properties. (This is a good reason to build objects all at once, as explained in Item 23.)
所以最后三个语句是错误的:
So the last three statements are errors:
constv={x:1,};v.x=3;// OKv.x='3';// ~ Type '"3"' is not assignable to type 'number'v.y=4;// ~ Property 'y' does not exist on type '{ x: number; }'v.name='Pythagoras';// ~~~~ Property 'name' does not exist on type '{ x: number; }'
constv={x:1,};v.x=3;// OKv.x='3';// ~ Type '"3"' is not assignable to type 'number'v.y=4;// ~ Property 'y' does not exist on type '{ x: number; }'v.name='Pythagoras';// ~~~~ Property 'name' does not exist on type '{ x: number; }'
同样,TypeScript 试图在特异性和灵活性之间取得平衡。它需要推断出足够具体的类型来捕获错误,但又不能具体到造成误报的程度。number它通过为初始化为类似值的属性推断类型来实现这一点1。
Again, TypeScript is trying to strike a balance between specificity and flexibility. It needs to infer a specific enough type to catch errors, but not so specific that it creates false positives. It does this by inferring a type of number for a property initialized to a value like 1.
如果您了解得更多,有几种方法可以覆盖 TypeScript 的默认行为。一种是提供显式类型注释:
If you know better, there are a few ways to override TypeScript’s default behavior. One is to supply an explicit type annotation:
constv:{x:1|3|5}={x:1,};// Type is { x: 1 | 3 | 5; }
constv:{x:1|3|5}={x:1,};// Type is { x: 1 | 3 | 5; }
另一个是为类型检查器提供额外的上下文(例如,通过将值作为函数的参数传递)。有关上下文在类型推断中的作用的更多信息,请参阅条目 26。
Another is to provide additional context to the type checker (e.g., by passing the value as the parameter of a function). For much more on the role of context in type inference, see Item 26.
A第三种方式是使用const断言。这不要与letand混淆const,后者在值空间中引入符号。这是一个纯粹的类型级构造。查看这些变量的不同推断类型:
A third way is with a const assertion. This is not to be confused with let and const, which introduce symbols in value space. This is a purely type-level construct. Look at the different inferred types for these variables:
constv1={x:1,y:2,};// Type is { x: number; y: number; }constv2={x:1asconst,y:2,};// Type is { x: 1; y: number; }constv3={x:1,y:2,}asconst;// Type is { readonly x: 1; readonly y: 2; }
constv1={x:1,y:2,};// Type is { x: number; y: number; }constv2={x:1asconst,y:2,};// Type is { x: 1; y: number; }constv3={x:1,y:2,}asconst;// Type is { readonly x: 1; readonly y: 2; }
当你as const在一个值之后写入时,TypeScript 会为它推断出最窄的可能类型。没有加宽。对于真正的常量,这通常是您想要的。您还可以使用as const数组来推断元组类型:
When you write as const after a value, TypeScript will infer the narrowest possible type for it. There is no widening. For true constants, this is typically what you want. You can also use as const with arrays to infer a tuple type:
consta1=[1,2,3];// Type is number[]consta2=[1,2,3]asconst;// Type is readonly [1, 2, 3]
consta1=[1,2,3];// Type is number[]consta2=[1,2,3]asconst;// Type is readonly [1, 2, 3]
如果您收到您认为是由于扩大引起的不正确错误,请考虑添加一些显式类型注释或const断言。在你的编辑器中检查类型是为此建立直觉的关键(见条款 6)。
If you’re getting incorrect errors that you think are due to widening, consider adding some explicit type annotations or const assertions. Inspecting types in your editor is the key to building an intuition for this (see Item 6).
这变宽的反义词是变窄。这是 TypeScript 从广义类型到狭义类型的过程。也许最常见的例子是空检查:
The opposite of widening is narrowing. This is the process by which TypeScript goes from a broad type to a narrower one. Perhaps the most common example of this is null checking:
constel=document.getElementById('foo');// Type is HTMLElement | nullif(el){el// Type is HTMLElementel.innerHTML='Party Time'.blink();}else{el// Type is nullalert('No element #foo');}
constel=document.getElementById('foo');// Type is HTMLElement | nullif(el){el// Type is HTMLElementel.innerHTML='Party Time'.blink();}else{el// Type is nullalert('No element #foo');}
如果el是null,那么第一个分支中的代码将不会执行。因此 TypeScript 能够null从该块中的类型联合中排除,从而产生更易于使用的更窄类型。类型检查器通常非常擅长缩小此类条件中的类型,尽管它偶尔会因别名而受阻(Item 24)。
If el is null, then the code in the first branch won’t execute. So TypeScript is able to exclude null from the type union within this block, resulting in a narrower type which is much easier to work with. The type checker is generally quite good at narrowing types in conditionals like these, though it can occasionally be thwarted by aliasing (Item 24).
您还可以通过抛出或从分支返回来缩小块其余部分的变量类型。例如:
You can also narrow a variable’s type for the rest of a block by throwing or returning from a branch. For example:
constel=document.getElementById('foo');// Type is HTMLElement | nullif(!el)thrownewError('Unable to find #foo');el;// Now type is HTMLElementel.innerHTML='Party Time'.blink();
constel=document.getElementById('foo');// Type is HTMLElement | nullif(!el)thrownewError('Unable to find #foo');el;// Now type is HTMLElementel.innerHTML='Party Time'.blink();
有很多方法可以缩小类型。使用instanceof作品:
There are many ways that you can narrow a type. Using instanceof works:
functioncontains(text:string,search:string|RegExp){if(searchinstanceofRegExp){search// Type is RegExpreturn!!search.exec(text);}search// Type is stringreturntext.includes(search);}
functioncontains(text:string,search:string|RegExp){if(searchinstanceofRegExp){search// Type is RegExpreturn!!search.exec(text);}search// Type is stringreturntext.includes(search);}
属性检查也是如此:
So does a property check:
interfaceA{a:number}interfaceB{b:number}functionpickAB(ab:A|B){if('a'inab){ab// Type is A}else{ab// Type is B}ab// Type is A | B}
interfaceA{a:number}interfaceB{b:number}functionpickAB(ab:A|B){if('a'inab){ab// Type is A}else{ab// Type is B}ab// Type is A | B}
一些内置函数例如Array.isArray能够缩小类型:
Some built-in functions such as Array.isArray are able to narrow types:
functioncontains(text:string,terms:string|string[]){consttermList=Array.isArray(terms)?terms:[terms];termList// Type is string[]// ...}
functioncontains(text:string,terms:string|string[]){consttermList=Array.isArray(terms)?terms:[terms];termList// Type is string[]// ...}
TypeScript 通常非常擅长通过条件来跟踪类型。在添加断言之前三思而后行——它可能在你不是的东西上!例如,这是null从联合类型中排除的错误方法:
TypeScript is generally quite good at tracking types through conditionals. Think twice before adding an assertion—it might be onto something that you’re not! For example, this is the wrong way to exclude null from a union type:
constel=document.getElementById('foo');// type is HTMLElement | nullif(typeofel==='object'){el;// Type is HTMLElement | null}
constel=document.getElementById('foo');// type is HTMLElement | nullif(typeofel==='object'){el;// Type is HTMLElement | null}
因为typeof null是"object"在 JavaScript 中,所以实际上您并没有排除null此检查!类似的惊喜可能来自虚假的原始值:
Because typeof null is "object" in JavaScript, you have not, in fact, excluded null with this check! Similar surprises can come from falsy primitive values:
functionfoo(x?:number|string|null){if(!x){x;// Type is string | number | null | undefined}}
functionfoo(x?:number|string|null){if(!x){x;// Type is string | number | null | undefined}}
因为空字符串 和0都是假的,x所以仍然可以是那个分支中的stringor 。numberTypeScript 是对的!
Because the empty string and 0 are both falsy, x could still be a string or number in that branch. TypeScript is right!
另一种帮助类型检查器缩小类型范围的常用方法是在它们上放置一个明确的“标签”:
Another common way to help the type checker narrow your types is by putting an explicit “tag” on them:
interfaceUploadEvent{type::::::'upload';filename_string;contents_string}interfaceDownloadEvent{type_'download';filename_string;}typeAppEvent=UploadEvent|DownloadEvent;functionhandleEvent(e:AppEvent){switch(e.type){case'download':e// Type is DownloadEventbreak;case'upload':e;// Type is UploadEventbreak;}}
interfaceUploadEvent{type:'upload';filename:string;contents:string}interfaceDownloadEvent{type:'download';filename:string;}typeAppEvent=UploadEvent|DownloadEvent;functionhandleEvent(e:AppEvent){switch(e.type){case'download':e// Type is DownloadEventbreak;case'upload':e;// Type is UploadEventbreak;}}
这pattern 被称为“tagged union”或“discriminated union”,它在 TypeScript 中无处不在。
This pattern is known as a “tagged union” or “discriminated union,” and it is ubiquitous in TypeScript.
如果 TypeScript 无法确定类型,您甚至可以引入自定义函数来帮助它:
If TypeScript isn’t able to figure out a type, you can even introduce a custom function to help it out:
functionisInputElement(el:HTMLElement):elisHTMLInputElement{return'value'inel;}functiongetElementContent(el:HTMLElement){if(isInputElement(el)){el;// Type is HTMLInputElementreturnel.value;}el;// Type is HTMLElementreturnel.textContent;}
functionisInputElement(el:HTMLElement):elisHTMLInputElement{return'value'inel;}functiongetElementContent(el:HTMLElement){if(isInputElement(el)){el;// Type is HTMLInputElementreturnel.value;}el;// Type is HTMLElementreturnel.textContent;}
这被称为“用户定义的类型保护”。asel is HTMLInputElement返回类型告诉类型检查器,如果函数返回 true,它可以缩小参数的类型。
This is known as a “user-defined type guard.” The el is HTMLInputElement as a return type tells the type checker that it can narrow the type of the parameter if the function returns true.
一些函数能够使用类型保护来跨数组或对象执行类型缩小。例如,如果您在数组中进行一些查找,您可能会得到一个可空类型的数组:
Some functions are able to use type guards to perform type narrowing across arrays or objects. If you do some lookups in an array, for instance, you may wind up with an array of nullable types:
constjackson5=['Jackie','Tito','Jermaine','Marlon','Michael'];constmembers=['Janet','Michael'].map(who=>jackson5.find(n=>n===who));// Type is (string | undefined)[]
constjackson5=['Jackie','Tito','Jermaine','Marlon','Michael'];constmembers=['Janet','Michael'].map(who=>jackson5.find(n=>n===who));// Type is (string | undefined)[]
undefined如果您使用过滤掉值filter,TypeScript 将无法执行:
If you filter out the undefined values using filter, TypeScript isn’t able to follow along:
constmembers=['Janet','Michael'].map(who=>jackson5.find(n=>n===who)).filter(who=>who!==undefined);// Type is (string | undefined)[]
constmembers=['Janet','Michael'].map(who=>jackson5.find(n=>n===who)).filter(who=>who!==undefined);// Type is (string | undefined)[]
但是如果你使用类型保护,它可以:
But if you use a type guard, it can:
functionisDefined<T>(x:T|undefined):xisT{returnx!==undefined;}constmembers=['Janet','Michael'].map(who=>jackson5.find(n=>n===who)).filter(isDefined);// Type is string[]
functionisDefined<T>(x:T|undefined):xisT{returnx!==undefined;}constmembers=['Janet','Michael'].map(who=>jackson5.find(n=>n===who)).filter(isDefined);// Type is string[]
与往常一样,在编辑器中检查类型是建立对缩小工作原理的直觉的关键。
As always, inspecting types in your editor is key to building an intuition for how narrowing works.
了解 TypeScript 中的类型如何变窄将帮助您对类型推断的工作方式建立直觉,理解错误,并通常与类型检查器建立更高效的关系。
Understanding how types in TypeScript narrow will help you build an intuition for how type inference works, make sense of errors, and generally have a more productive relationship with the type checker.
作为 Item 20解释说,虽然变量的值可能会改变,但它在 TypeScript 中的类型通常不会改变。这使得一些 JavaScript 模式比其他模式更容易在 TypeScript 中建模。特别是,这意味着您应该更喜欢一次创建所有对象,而不是逐个创建对象。
As Item 20 explained, while a variable’s value may change, its type in TypeScript generally does not. This makes some JavaScript patterns easier to model in TypeScript than others. In particular, it means that you should prefer creating objects all at once, rather than piece by piece.
下面是在 JavaScript 中创建表示二维点的对象的一种方法:
Here’s one way to create an object representing a two-dimensional point in JavaScript:
constpt={};pt.x=3;pt.y=4;
constpt={};pt.x=3;pt.y=4;
在 TypeScript 中,这将在每次赋值时产生错误:
In TypeScript, this will produce errors on each assignment:
constpt={};pt.x=3;// ~ Property 'x' does not exist on type '{}'pt.y=4;// ~ Property 'y' does not exist on type '{}'
constpt={};pt.x=3;// ~ Property 'x' does not exist on type '{}'pt.y=4;// ~ Property 'y' does not exist on type '{}'
pt这是因为第一行的类型是根据其值推断的{},您只能分配给已知属性。
This is because the type of pt on the first line is inferred based on its value {}, and you may only assign to known properties.
如果你定义一个接口,你会遇到相反的问题Point:
You get the opposite problem if you define a Point interface:
interfacePoint{x:number;y:number;}constpt:Point={};// ~~ Type '{}' is missing the following properties from type 'Point': x, ypt.x=3;pt.y=4;
interfacePoint{x:number;y:number;}constpt:Point={};// ~~ Type '{}' is missing the following properties from type 'Point': x, ypt.x=3;pt.y=4;
解决方案是一次定义所有对象:
The solution is to define the object all at once:
constpt={x:3,y:4,};// OK
constpt={x:3,y:4,};// OK
如果必须零碎地构建对象,可以使用类型断言 ( as) 来使类型检查器静音:
If you must build the object piecemeal, you may use a type assertion (as) to silence the type checker:
constpt={}asPoint;pt.x=3;pt.y=4;// OK
constpt={}asPoint;pt.x=3;pt.y=4;// OK
但更好的方法是一次性构建所有对象并使用声明(参见条目 9):
But the better way is by building the object all at once and using a declaration (see Item 9):
constpt:Point={x:3,y:4,};
constpt:Point={x:3,y:4,};
如果您需要从较小的对象构建较大的对象,请避免分多个步骤进行:
If you need to build a larger object from smaller ones, avoid doing it in multiple steps:
constpt={x:3,y:4};constid={name:'Pythagoras'};constnamedPoint={};Object.assign(namedPoint,pt,id);namedPoint.name;// ~~~~ Property 'name' does not exist on type '{}'
constpt={x:3,y:4};constid={name:'Pythagoras'};constnamedPoint={};Object.assign(namedPoint,pt,id);namedPoint.name;// ~~~~ Property 'name' does not exist on type '{}'
你可以一次构建更大的对象,而不是使用对象展开运算符, ...:
You can build the larger object all at once instead using the object spread operator, ...:
constnamedPoint={...pt,...id};namedPoint.name;// OK, type is string
constnamedPoint={...pt,...id};namedPoint.name;// OK, type is string
您还可以使用对象展开运算符以类型安全的方式逐字段构建对象。关键是在每次更新时使用一个新变量,以便每个更新都有一个新类型:
You can also use the object spread operator to build up objects field by field in a type-safe way. The key is to use a new variable on every update so that each gets a new type:
constpt0={};constpt1={...pt0,x:3};constpt:Point={...pt1,y:4};// OK
constpt0={};constpt1={...pt0,x:3};constpt:Point={...pt1,y:4};// OK
虽然这是构建如此简单对象的一种迂回方式,但它可以成为向对象添加属性并允许 TypeScript 推断新类型的有用技术。
While this is a roundabout way to build up such a simple object, it can be a useful technique for adding properties to an object and allowing TypeScript to infer a new type.
要以类型安全的方式有条件地添加属性,您可以使用带有nullor 的对象扩展{},它不添加任何属性:
To conditionally add a property in a type-safe way, you can use object spread with null or {}, which add no properties:
declarelethasMiddle:boolean;constfirstLast={first:'Harry',last:'Truman'};constpresident={...firstLast,...(hasMiddle?{middle:'S'}:{})};
declarelethasMiddle:boolean;constfirstLast={first:'Harry',last:'Truman'};constpresident={...firstLast,...(hasMiddle?{middle:'S'}:{})};
如果将鼠标悬停president在编辑器中,您会看到它的类型被推断为联合:
If you mouse over president in your editor, you’ll see that its type is inferred as a union:
constpresident:{middle::::::string;first_string;last_string;}|{first_string;last_string;}
constpresident:{middle:string;first:string;last:string;}|{first:string;last:string;}
middle如果您想成为一个可选字段,这可能会让人感到意外。你不能读出middle这种类型,例如:
This may come as a surprise if you wanted middle to be an optional field. You can’t read middle off this type, for example:
president.middle// ~~~~~~ Property 'middle' does not exist on type// '{ first: string; last: string; }'
president.middle// ~~~~~~ Property 'middle' does not exist on type// '{ first: string; last: string; }'
如果您有条件地添加多个属性,联合会更准确地表示可能值的集合(第 32 项)。但是可选字段会更容易使用。你可以得到一个帮手:
If you’re conditionally adding multiple properties, the union does more accurately represent the set of possible values (Item 32). But an optional field would be easier to work with. You can get one with a helper:
functionaddOptional<Textendsobject,Uextendsobject>(a:T,b:U|null):T&Partial<U>{return{...a,...b};}constpresident=addOptional(firstLast,hasMiddle?{middle:'S'}:null);president.middle// OK, type is string | undefined
functionaddOptional<Textendsobject,Uextendsobject>(a:T,b:U|null):T&Partial<U>{return{...a,...b};}constpresident=addOptional(firstLast,hasMiddle?{middle:'S'}:null);president.middle// OK, type is string | undefined
有时您想通过转换另一个对象或数组来构建对象或数组。在这种情况下,“一次构建所有对象”的等价物是使用内置函数结构或实用程序库(如 Lodash)而不是循环。有关更多信息,请参阅第 27 项。
Sometimes you want to build an object or array by transforming another one. In this case the equivalent of “building objects all at once” is using built-in functional constructs or utility libraries like Lodash rather than loops. See Item 27 for more on this.
When you introduce a new name for a value:
constborough={name:'Brooklyn',location:[40.688,-73.979]};constloc=borough.location;
constborough={name:'Brooklyn',location:[40.688,-73.979]};constloc=borough.location;
你已经创建了一个别名。对别名属性的更改也将在原始值上可见:
you have created an alias. Changes to properties on the alias will be visible on the original value as well:
>位置 [0] = 0; > borough.location [0, -73.979]
> loc[0] = 0; > borough.location [0, -73.979]
别名是所有语言的编译器编写者的祸根,因为它们使控制流分析变得困难。如果你有意使用别名,TypeScript 将能够更好地理解你的代码并帮助你发现更多真正的错误。
Aliases are the bane of compiler writers in all languages because they make control flow analysis difficult. If you’re deliberate in your use of aliases, TypeScript will be able to understand your code better and help you find more real errors.
假设您有一个表示多边形的数据结构:
Suppose you have a data structure that represents a polygon:
interfaceCoordinate{x:number;y:number;}interfaceBoundingBox{x:[number,number];y:[number,number];}interfacePolygon{exterior:Coordinate[];holes:Coordinate[][];bbox?:BoundingBox;}
interfaceCoordinate{x:number;y:number;}interfaceBoundingBox{x:[number,number];y:[number,number];}interfacePolygon{exterior:Coordinate[];holes:Coordinate[][];bbox?:BoundingBox;}
多边形的几何形状由exterior和holes属性指定。该bbox属性是可能存在也可能不存在的优化。您可以使用它来加速多边形中的点检查:
The geometry of the polygon is specified by the exterior and holes properties. The bbox property is an optimization that may or may not be present. You can use it to speed up a point-in-polygon check:
functionisPointInPolygon(polygon:Polygon,pt:Coordinate){if(polygon.bbox){if(pt.x<polygon.bbox.x[0]||pt.x>polygon.bbox.x[1]||pt.y<polygon.bbox.y[1]||pt.y>polygon.bbox.y[1]){returnfalse;}}// ... more complex check}
functionisPointInPolygon(polygon:Polygon,pt:Coordinate){if(polygon.bbox){if(pt.x<polygon.bbox.x[0]||pt.x>polygon.bbox.x[1]||pt.y<polygon.bbox.y[1]||pt.y>polygon.bbox.y[1]){returnfalse;}}// ... more complex check}
此代码有效(和类型检查)但有点重复:polygon.bbox在三行中出现五次!这是尝试分解出中间变量以减少重复的尝试:
This code works (and type checks) but is a bit repetitive: polygon.bbox appears five times in three lines! Here’s an attempt to factor out an intermediate variable to reduce duplication:
functionisPointInPolygon(polygon:Polygon,pt:Coordinate){constbox=polygon.bbox;if(polygon.bbox){if(pt.x<box.x[0]||pt.x>box.x[1]||// ~~~ ~~~ Object is possibly 'undefined'pt.y<box.y[1]||pt.y>box.y[1]){// ~~~ ~~~ Object is possibly 'undefined'returnfalse;}}// ...}
functionisPointInPolygon(polygon:Polygon,pt:Coordinate){constbox=polygon.bbox;if(polygon.bbox){if(pt.x<box.x[0]||pt.x>box.x[1]||// ~~~ ~~~ Object is possibly 'undefined'pt.y<box.y[1]||pt.y>box.y[1]){// ~~~ ~~~ Object is possibly 'undefined'returnfalse;}}// ...}
(我假设你已经启用了strictNullChecks。)
(I’m assuming you’ve enabled strictNullChecks.)
这段代码仍然有效,那么为什么会出错呢?通过分解box变量,您为 创建了一个别名polygon.bbox,这阻碍了在第一个示例中悄悄进行的控制流分析。
This code still works, so why the error? By factoring out the box variable, you’ve created an alias for polygon.bbox, and this has thwarted the control flow analysis that quietly worked in the first example.
box您可以检查和的类型polygon.bbox以查看发生了什么:
You can inspect the types of box and polygon.bbox to see what’s happening:
functionisPointInPolygon(polygon:Polygon,pt:Coordinate){polygon.bbox// Type is BoundingBox | undefinedconstbox=polygon.bbox;box// Type is BoundingBox | undefinedif(polygon.bbox){polygon.bbox// Type is BoundingBoxbox// Type is BoundingBox | undefined}}
functionisPointInPolygon(polygon:Polygon,pt:Coordinate){polygon.bbox// Type is BoundingBox | undefinedconstbox=polygon.bbox;box// Type is BoundingBox | undefinedif(polygon.bbox){polygon.bbox// Type is BoundingBoxbox// Type is BoundingBox | undefined}}
属性检查细化了的类型polygon.bbox但不细化box了错误。这引出了别名的黄金法则:如果您引入别名,请 始终如一地使用它。
The property check refines the type of polygon.bbox but not of box and hence the errors. This leads us to the golden rule of aliasing: if you introduce an alias, use it consistently.
在属性检查中使用box可修复错误:
Using box in the property check fixes the error:
functionisPointInPolygon(polygon:Polygon,pt:Coordinate){constbox=polygon.bbox;if(box){if(pt.x<box.x[0]||pt.x>box.x[1]||pt.y<box.y[1]||pt.y>box.y[1]){// OKreturnfalse;}}// ...}
functionisPointInPolygon(polygon:Polygon,pt:Coordinate){constbox=polygon.bbox;if(box){if(pt.x<box.x[0]||pt.x>box.x[1]||pt.y<box.y[1]||pt.y>box.y[1]){// OKreturnfalse;}}// ...}
类型检查器现在很高兴,但是对于人类读者来说有一个问题。我们为同一事物使用两个名称:box和bbox。这是一个没有区别的区别(第 36 项)。
The type checker is happy now, but there’s an issue for human readers. We’re using two names for the same thing: box and bbox. This is a distinction without a difference (Item 36).
对象解构语法以更紧凑的语法奖励一致的命名。您甚至可以在数组和嵌套结构上使用它:
Object destructuring syntax rewards consistent naming with a more compact syntax. You can even use it on arrays and nested structures:
functionisPointInPolygon(polygon:Polygon,pt:Coordinate){const{bbox}=polygon;if(bbox){const{x,y}=bbox;if(pt.x<x[0]||pt.x>x[1]||pt.y<x[0]||pt.y>y[1]){returnfalse;}}// ...}
functionisPointInPolygon(polygon:Polygon,pt:Coordinate){const{bbox}=polygon;if(bbox){const{x,y}=bbox;if(pt.x<x[0]||pt.x>x[1]||pt.y<x[0]||pt.y>y[1]){returnfalse;}}// ...}
其他几点:
A few other points:
x如果和y属性是可选的,而不是整个属性,则此代码将需要更多的属性检查bbox。我们受益于遵循条款 31的建议,该条款讨论了将空值推送到类型边界的重要性。
This code would have required more property checks if the x and y properties had been optional, rather than the whole bbox property. We benefited from following the advice of Item 31, which discusses the importance of pushing null values to the perimeter of your types.
可选属性适合bbox但不适合holes。如果holes是可选的,那么它可能会丢失或为空数组 ( [])。这将是一个没有区别的区别。空数组是表示“无孔”的好方法。
An optional property was appropriate for bbox but would not have been appropriate for holes. If holes was optional, then it would be possible for it to be either missing or an empty array ([]). This would be a distinction without a difference. An empty array is a fine way to indicate “no holes.”
在与类型检查器的交互中,不要忘记别名也会在运行时引入混淆:
In your interactions with the type checker, don’t forget that aliasing can introduce confusion at runtime, too:
const{bbox}=polygon;if(!bbox){calculatePolygonBbox(polygon);// Fills in polygon.bbox// Now polygon.bbox and bbox refer to different values!}
const{bbox}=polygon;if(!bbox){calculatePolygonBbox(polygon);// Fills in polygon.bbox// Now polygon.bbox and bbox refer to different values!}
TypeScript 的控制流分析往往对局部变量非常有用。但是对于属性你应该警惕:
TypeScript’s control flow analysis tends to be quite good for local variables. But for properties you should be on guard:
functionfn(p:Polygon){/* ... */}polygon.bbox// Type is BoundingBox | undefinedif(polygon.bbox){polygon.bbox// Type is BoundingBoxfn(polygon);polygon.bbox// Type is still BoundingBox}
functionfn(p:Polygon){/* ... */}polygon.bbox// Type is BoundingBox | undefinedif(polygon.bbox){polygon.bbox// Type is BoundingBoxfn(polygon);polygon.bbox// Type is still BoundingBox}
这调用fn(polygon)可以很好地取消设置polygon.bbox,因此将类型恢复为 会更安全BoundingBox | undefined。但这会令人沮丧:每次调用函数时都必须重复属性检查。因此 TypeScript 做出了务实的选择,假设该函数不会使其类型细化无效。但它可以。如果您分解出局部bbox变量而不是使用polygon.bbox,则类型bbox将保持准确,但它可能不再是与 相同的值polygon.box。
The call to fn(polygon) could very well un-set polygon.bbox, so it would be safer for the type to revert to BoundingBox | undefined. But this would get frustrating: you’d have to repeat your property checks every time you called a function. So TypeScript makes the pragmatic choice to assume the function does not invalidate its type refinements. But it could. If you’d factored out a local bbox variable instead of using polygon.bbox, the type of bbox would remain accurate, but it might no longer be the same value as polygon.box.
别名可以防止 TypeScript 缩小类型。如果您为变量创建别名,请始终如一地使用它。
Aliasing can prevent TypeScript from narrowing types. If you create an alias for a variable, use it consistently.
使用解构语法来鼓励一致的命名。
Use destructuring syntax to encourage consistent naming.
Be aware of how function calls can invalidate type refinements on properties. Trust refinements on local variables more than on properties.
经典的JavaScript 使用回调对异步行为进行建模。这导致了臭名昭著的“厄运金字塔”:
Classic JavaScript modeled asynchronous behavior using callbacks. This leads to the infamous “pyramid of doom”:
fetchURL(url1,function(response1){fetchURL(url2,function(response2){fetchURL(url3,function(response3){// ...console.log(1);});console.log(2);});console.log(3);});console.log(4);// Logs:// 4// 3// 2// 1
fetchURL(url1,function(response1){fetchURL(url2,function(response2){fetchURL(url3,function(response3){// ...console.log(1);});console.log(2);});console.log(3);});console.log(4);// Logs:// 4// 3// 2// 1
从日志中可以看出,执行顺序与代码顺序相反。这使得回调代码难以阅读。如果您想并行运行请求或在发生错误时退出,它会变得更加混乱。
As you can see from the logs, the execution order is the opposite of the code order. This makes callback code hard to read. It gets even more confusing if you want to run the requests in parallel or bail when an error occurs.
ES2015引入了打破厄运金字塔的承诺的概念。Promise 代表将来可用的东西(它们有时也称为“期货”)。下面是使用 Promises 的相同代码:
ES2015 introduced the concept of a Promise to break the pyramid of doom. A Promise represents something that will be available in the future (they’re also sometimes called “futures”). Here’s the same code using Promises:
constpage1Promise=fetch(url1);page1Promise.then(response1=>{returnfetch(url2);}).then(response2=>{returnfetch(url3);}).then(response3=>{// ...}).catch(error=>{// ...});
constpage1Promise=fetch(url1);page1Promise.then(response1=>{returnfetch(url2);}).then(response2=>{returnfetch(url3);}).then(response3=>{// ...}).catch(error=>{// ...});
现在嵌套少了,执行顺序更直接匹配代码顺序。整合错误处理和使用Promise.all.
Now there’s less nesting, and the execution order more directly matches the code order. It’s also easier to consolidate error handling and use higher-order tools like Promise.all.
ES2017引入了async和await关键字使事情变得更简单:
ES2017 introduced the async and await keywords to make things even simpler:
asyncfunctionfetchPages() {constresponse1=awaitfetch(url1);constresponse2=awaitfetch(url2);constresponse3=awaitfetch(url3);// ...}
asyncfunctionfetchPages() {constresponse1=awaitfetch(url1);constresponse2=awaitfetch(url2);constresponse3=awaitfetch(url3);// ...}
关键字await暂停fetchPages函数的执行,直到每个 Promise 都 resolve 为止。在一个async函数中,await调用一个抛出异常的 Promise。这使您可以使用通常的 try/catch 机制:
The await keyword pauses execution of the fetchPages function until each Promise resolves. Within an async function, awaiting a Promise that throws an exception. This lets you use the usual try/catch machinery:
asyncfunctionfetchPages() {try{constresponse1=awaitfetch(url1);constresponse2=awaitfetch(url2);constresponse3=awaitfetch(url3);// ...}catch(e){// ...}}
asyncfunctionfetchPages() {try{constresponse1=awaitfetch(url1);constresponse2=awaitfetch(url2);constresponse3=awaitfetch(url3);// ...}catch(e){// ...}}
什么时候你的目标是 ES5 或更早版本,TypeScript 编译器将执行一些精心设计的转换来制作async和await工作。换句话说,无论你的运行时是什么,你都可以使用 TypeScript async/ await。
When you target ES5 or earlier, the TypeScript compiler will perform some elaborate transformations to make async and await work. In other words, whatever your runtime, with TypeScript you can use async/await.
有几个很好的理由更喜欢 Promises 或async/await而不是回调:
There are a few good reasons to prefer Promises or async/await to callbacks:
承诺比回调更容易编写。
Promises are easier to compose than callbacks.
类型能够比回调更容易地流过 Promises。
Types are able to flow through Promises more easily than callbacks.
例如,如果你想并行获取页面,你可以将 Promises 组合成Promise.all:
If you want to fetch the pages in parallel, for example, you can compose Promises with Promise.all:
asyncfunctionfetchPages() {const[response1,response2,response3]=awaitPromise.all([fetch(url1),fetch(url2),fetch(url3)]);// ...}
asyncfunctionfetchPages() {const[response1,response2,response3]=awaitPromise.all([fetch(url1),fetch(url2),fetch(url3)]);// ...}
在这种情况下使用解构赋值await特别好。
Using destructuring assignment with await is particularly nice in this context.
TypeScript 能够将三个response变量中的每一个的类型推断为Response. 与回调并行执行请求的等效代码需要更多机制和类型注释:
TypeScript is able to infer the types of each of the three response variables as Response. The equivalent code to do the requests in parallel with callbacks requires more machinery and a type annotation:
functionfetchPagesCB() {letnumDone=0;constresponses:string[]=[];constdone=()=>{const[response1,response2,response3]=responses;// ...};consturls=[url1,url2,url3];urls.forEach((url,i)=>{fetchURL(url,r=>{responses[i]=url;numDone++;if(numDone===urls.length)done();});});}
functionfetchPagesCB() {letnumDone=0;constresponses:string[]=[];constdone=()=>{const[response1,response2,response3]=responses;// ...};consturls=[url1,url2,url3];urls.forEach((url,i)=>{fetchURL(url,r=>{responses[i]=url;numDone++;if(numDone===urls.length)done();});});}
将其扩展到包括错误处理或尽可能通用是具Promise.all有挑战性的。
Extending this to include error handling or to be as generic as Promise.all is challenging.
类型推断也适用于Promise.race,它在其第一个输入 Promises 解析时解析。您可以使用它以一般方式向 Promises 添加超时:
Type inference also works well with Promise.race, which resolves when the first of its input Promises resolves. You can use this to add timeouts to Promises in a general way:
functiontimeout(millis:number):Promise<never>{returnnewPromise((resolve,reject)=>{setTimeout(()=>reject('timeout'),millis);});}asyncfunctionfetchWithTimeout(url:string,ms:number){returnPromise.race([fetch(url),timeout(ms)]);}
functiontimeout(millis:number):Promise<never>{returnnewPromise((resolve,reject)=>{setTimeout(()=>reject('timeout'),millis);});}asyncfunctionfetchWithTimeout(url:string,ms:number){returnPromise.race([fetch(url),timeout(ms)]);}
的返回类型fetchWithTimeout被推断为Promise<Response>,不需要类型注释。深入探究其工作原理很有趣:Promise.race在本例中, 的返回类型是其输入类型的并集Promise<Response | never>。但是与(空集)合并never是一个空操作,所以这被简化为Promise<Response>. 当您使用 Promises 时,TypeScript 的所有类型推断机制都会为您提供正确的类型。
The return type of fetchWithTimeout is inferred as Promise<Response>, no type annotations required. It’s interesting to dig into why this works: the return type of Promise.race is the union of the types of its inputs, in this case Promise<Response | never>. But taking a union with never (the empty set) is a no-op, so this gets simplified to Promise<Response>. When you work with Promises, all of TypeScript’s type inference machinery works to get you the right types.
有时您需要使用原始 Promises,特别是当您包装回调 API 时,例如setTimeout. 但是如果你有选择的话,你通常应该更喜欢async/await而不是原始的 Promise,原因有两个:
There are some times when you need to use raw Promises, notably when you are wrapping a callback API like setTimeout. But if you have a choice, you should generally prefer async/await to raw Promises for two reasons:
它通常会生成更简洁直接的代码。
It typically produces more concise and straightforward code.
它强制async函数总是返回 Promise。
It enforces that async functions always return Promises.
函数async总是返回 a Promise,即使它不涉及await任何东西。TypeScript 可以帮助您建立直觉:
An async function always returns a Promise, even if it doesn’t involve awaiting anything. TypeScript can help you build an intuition for this:
// function getNumber(): Promise<number>asyncfunctiongetNumber() {return42;}
// function getNumber(): Promise<number>asyncfunctiongetNumber() {return42;}
You can also create async arrow functions:
constgetNumber=async()=>42;// Type is () => Promise<number>
constgetNumber=async()=>42;// Type is () => Promise<number>
原始的 Promise 等价物是:
The raw Promise equivalent is:
constgetNumber=()=>Promise.resolve(42);// Type is () => Promise<number>
constgetNumber=()=>Promise.resolve(42);// Type is () => Promise<number>
虽然为立即可用的值返回一个 Promise 看起来很奇怪,但这实际上有助于执行一个重要的规则:函数应该始终同步运行或始终异步运行。它不应该将两者混为一谈。例如,如果要为fetchURL函数添加缓存怎么办?这是一个尝试:
While it may seem odd to return a Promise for an immediately available value, this actually helps enforce an important rule: a function should either always be run synchronously or always be run asynchronously. It should never mix the two. For example, what if you want to add a cache to the fetchURL function? Here’s an attempt:
// Don't do this!const_cache:{[url:string]:string}={};functionfetchWithCache(url:string,callback:(text:string)=>void){if(urlin_cache){callback(_cache[url]);}else{fetchURL(url,text=>{_cache[url]=text;callback(text);});}}
// Don't do this!const_cache:{[url:string]:string}={};functionfetchWithCache(url:string,callback:(text:string)=>void){if(urlin_cache){callback(_cache[url]);}else{fetchURL(url,text=>{_cache[url]=text;callback(text);});}}
虽然这看起来像是一种优化,但该功能现在对于客户来说极难使用:
While this may seem like an optimization, the function is now extremely difficult for a client to use:
letrequestStatus:'loading'|'success'|'error';functiongetUser(userId:string){fetchWithCache(`/user/${userId}`,profile=>{requestStatus='success';});requestStatus='loading';}
letrequestStatus:'loading'|'success'|'error';functiongetUser(userId:string){fetchWithCache(`/user/${userId}`,profile=>{requestStatus='success';});requestStatus='loading';}
requestStatus调用后的值是多少getUser?这完全取决于配置文件是否被缓存。如果不是,requestStatus将设置为“成功”。如果是,它将设置为“成功”,然后设置回“正在加载”。哎呀!
What will the value of requestStatus be after calling getUser? It depends entirely on whether the profile is cached. If it’s not, requestStatus will be set to “success.” If it is, it’ll get set to “success” and then set back to “loading.” Oops!
async对这两个函数使用强制执行一致的行为:
Using async for both functions enforces consistent behavior:
const_cache:{[url:string]:string}={};asyncfunctionfetchWithCache(url:string){if(urlin_cache){return_cache[url];}constresponse=awaitfetch(url);consttext=awaitresponse.text();_cache[url]=text;returntext;}letrequestStatus:'loading'|'success'|'error';asyncfunctiongetUser(userId:string){requestStatus='loading';constprofile=awaitfetchWithCache(`/user/${userId}`);requestStatus='success';}
const_cache:{[url:string]:string}={};asyncfunctionfetchWithCache(url:string){if(urlin_cache){return_cache[url];}constresponse=awaitfetch(url);consttext=awaitresponse.text();_cache[url]=text;returntext;}letrequestStatus:'loading'|'success'|'error';asyncfunctiongetUser(userId:string){requestStatus='loading';constprofile=awaitfetchWithCache(`/user/${userId}`);requestStatus='success';}
现在它完全透明,requestStatus将以“成功”告终。使用回调或原始 Promises 很容易意外生成半同步代码,但使用async.
Now it’s completely transparent that requestStatus will end in “success.” It’s easy to accidentally produce half-synchronous code with callbacks or raw Promises, but difficult with async.
请注意,如果您从async函数返回一个 Promise,它不会被包装在另一个 Promise 中:返回类型将是Promise<T>而不是Promise<Promise<T>>。同样,TypeScript 将帮助您为此建立直觉:
Note that if you return a Promise from an async function, it will not get wrapped in another Promise: the return type will be Promise<T> rather than Promise<Promise<T>>. Again, TypeScript will help you build an intuition for this:
// Function getJSON(url: string): Promise<any>asyncfunctiongetJSON(url:string){constresponse=awaitfetch(url);constjsonPromise=response.json();// Type is Promise<any>returnjsonPromise;}
// Function getJSON(url: string): Promise<any>asyncfunctiongetJSON(url:string){constresponse=awaitfetch(url);constjsonPromise=response.json();// Type is Promise<any>returnjsonPromise;}
优先使用 Promises 而不是回调以获得更好的可组合性和类型流。
Prefer Promises to callbacks for better composability and type flow.
在可能的情况下,优先使用asyncandawait而不是原始的 Promise。它们产生更简洁、直接的代码并消除了整类错误。
Prefer async and await to raw Promises when possible. They produce more concise, straightforward code and eliminate whole classes of errors.
TypeScript 不只是根据值推断类型。它还会考虑值出现的上下文。这通常很有效,但有时会导致意外。了解上下文在类型推断中的使用方式将帮助您在这些意外发生时识别并解决它们。
TypeScript doesn’t just infer types based on values. It also considers the context in which the value occurs. This usually works well but can sometimes lead to surprises. Understanding how context is used in type inference will help you identify and work around these surprises when they do occur.
在 JavaScript 中,您可以在不更改代码行为的情况下将表达式分解为常量(只要您不更改执行顺序)。换句话说,这两个语句是等价的:
In JavaScript you can factor an expression out into a constant without changing the behavior of your code (so long as you don’t alter execution order). In other words, these two statements are equivalent:
// Inline formsetLanguage('JavaScript');// Reference formletlanguage='JavaScript';setLanguage(language);
// Inline formsetLanguage('JavaScript');// Reference formletlanguage='JavaScript';setLanguage(language);
在 TypeScript 中,这个重构仍然有效:
In TypeScript, this refactor still works:
functionsetLanguage(language:string){/* ... */}setLanguage('JavaScript');// OKletlanguage='JavaScript';setLanguage(language);// OK
functionsetLanguage(language:string){/* ... */}setLanguage('JavaScript');// OKletlanguage='JavaScript';setLanguage(language);// OK
现在假设您将条款 33的建议放在心上,并用更精确的字符串文字类型联合替换字符串类型:
Now suppose you take to heart the advice of Item 33 and replace the string type with a more precise union of string literal types:
typeLanguage='JavaScript'|'TypeScript'|'Python';functionsetLanguage(language:Language){/* ... */}setLanguage('JavaScript');// OKletlanguage='JavaScript';setLanguage(language);// ~~~~~~~~ Argument of type 'string' is not assignable// to parameter of type 'Language'
typeLanguage='JavaScript'|'TypeScript'|'Python';functionsetLanguage(language:Language){/* ... */}setLanguage('JavaScript');// OKletlanguage='JavaScript';setLanguage(language);// ~~~~~~~~ Argument of type 'string' is not assignable// to parameter of type 'Language'
什么地方出了错?使用内联形式,TypeScript 从函数声明中知道参数应该是 type Language。字符串文字'JavaScript'可分配给此类型,所以这没问题。但是当你分解出一个变量时,TypeScript 必须在赋值时推断它的类型。在这种情况下,它推断string, which is not assignable to Language. 因此错误。
What went wrong? With the inline form, TypeScript knows from the function declaration that the parameter is supposed to be of type Language. The string literal 'JavaScript' is assignable to this type, so this is OK. But when you factor out a variable, TypeScript must infer its type at the time of assignment. In this case it infers string, which is not assignable to Language. Hence the error.
(有些语言能够根据变量的最终用途推断变量的类型。但这也可能令人困惑。Anders Hejlsberg,TypeScript 的创建者将其称为“远距离的幽灵般的动作”。总的来说,TypeScript 在首次引入变量时就确定了变量的类型。对于此规则的一个值得注意的例外,请参阅条目 41。)
(Some languages are able to infer types for variables based on their eventual usage. But this can also be confusing. Anders Hejlsberg, the creator of TypeScript, refers to it as “spooky action at a distance.” By and large, TypeScript determines the type of a variable when it is first introduced. For a notable exception to this rule, see Item 41.)
有两种解决这个问题的好方法。language一种是用类型声明来限制可能的值:
There are two good ways to solve this problem. One is to constrain the possible values of language with a type declaration:
letlanguage:Language='JavaScript';setLanguage(language);// OK
letlanguage:Language='JavaScript';setLanguage(language);// OK
这还有一个好处,即在语言中出现拼写错误时标记错误——例如'Typescript'(它应该是大写的“S”)。
This also has the benefit of flagging an error if there’s a typo in the language—for example 'Typescript' (it should be a capital “S”).
另一种解决方案是使变量常量:
The other solution is to make the variable constant:
constlanguage='JavaScript';setLanguage(language);// OK
constlanguage='JavaScript';setLanguage(language);// OK
通过使用const,我们已经告诉类型检查器这个变量不能改变。因此 TypeScript 可以推断出更精确的类型,即language字符串文字类型"JavaScript"。这是可分配的,Language因此代码类型检查。当然,如果确实需要重新赋值language,则需要使用类型声明。(有关更多信息,请参阅第 21 项。)
By using const, we’ve told the type checker that this variable cannot change. So TypeScript can infer a more precise type for language, the string literal type "JavaScript". This is assignable to Language so the code type checks. Of course, if you do need to reassign language, then you’ll need to use the type declaration. (For more on this, see Item 21.)
这里的根本问题是我们已经将值从使用它的上下文中分离出来。有时这是可以的,但通常不是。本项目的其余部分将介绍这种上下文丢失可能导致错误的几种情况,并向您展示如何修复这些错误。
The fundamental issue here is that we’ve separated the value from the context in which it’s used. Sometimes this is OK, but often it is not. The rest of this item walks through a few cases where this loss of context can cause errors and shows you how to fix them.
在除了字符串文字类型之外,元组类型也会出现问题。假设您正在使用可让您以编程方式平移地图的地图可视化:
In addition to string literal types, problems can come up with tuple types. Suppose you’re working with a map visualization that lets you programmatically pan the map:
// Parameter is a (latitude, longitude) pair.functionpanTo(where:[number,number]){/* ... */}panTo([10,20]);// OKconstloc=[10,20];panTo(loc);// ~~~ Argument of type 'number[]' is not assignable to// parameter of type '[number, number]'
// Parameter is a (latitude, longitude) pair.functionpanTo(where:[number,number]){/* ... */}panTo([10,20]);// OKconstloc=[10,20];panTo(loc);// ~~~ Argument of type 'number[]' is not assignable to// parameter of type '[number, number]'
和以前一样,您已经将值与其上下文分开。在第一个实例中[10, 20]是可分配给元组类型的[number, number]。loc在第二个中,TypeScript 推断as的类型number[](即未知长度的数组)。这不能分配给元组类型,因为许多数组的元素数量不正确。
As before, you’ve separated a value from its context. In the first instance [10, 20] is assignable to the tuple type [number, number]. In the second, TypeScript infers the type of loc as number[] (i.e., an array of numbers of unknown length). This is not assignable to the tuple type, since many arrays have the wrong number of elements.
所以如何在不求助于的情况下修复此错误any?你已经声明了const,所以这无济于事。但是你仍然可以提供一个类型声明来让 TypeScript 准确地知道你的意思:
So how can you fix this error without resorting to any? You’ve already declared it const, so that won’t help. But you can still provide a type declaration to let TypeScript know precisely what you mean:
constloc:[number,number]=[10,20];panTo(loc);// OK
constloc:[number,number]=[10,20];panTo(loc);// OK
另一种方法是提供“常量上下文”。这会告诉 TypeScript 您希望该值是深度常量,而不是const给出的浅常量:
Another way is to provide a “const context.” This tells TypeScript that you intend the value to be deeply constant, rather than the shallow constant that const gives:
constloc=[10,20]asconst;panTo(loc);// ~~~ Type 'readonly [10, 20]' is 'readonly'// and cannot be assigned to the mutable type '[number, number]'
constloc=[10,20]asconst;panTo(loc);// ~~~ Type 'readonly [10, 20]' is 'readonly'// and cannot be assigned to the mutable type '[number, number]'
如果将鼠标悬停loc在编辑器中,您会看到它的类型现在被推断为readonly [10, 20],而不是number[]。不幸的是,这太精确了!的类型签名panTo不承诺它不会修改其where参数的内容。因为loc参数有readonly类型,所以这是行不通的。这里最好的解决方案是readonly为函数添加注释panTo:
If you hover over loc in your editor, you’ll see that its type is now inferred as readonly [10, 20], rather than number[]. Unfortunately this is too precise! The type signature of panTo makes no promises that it won’t modify the contents of its where parameter. Since the loc parameter has a readonly type, this won’t do. The best solution here is to add a readonly annotation to the panTo function:
functionpanTo(where:readonly[number,number]){/* ... */}constloc=[10,20]asconst;panTo(loc);// OK
functionpanTo(where:readonly[number,number]){/* ... */}constloc=[10,20]asconst;panTo(loc);// OK
如果类型签名不在您的控制范围内,那么您将需要使用注释。
If the type signature is outside your control, then you’ll need to use an annotation.
const上下文可以很好地解决推理中丢失上下文的问题,但它们确实有一个不幸的缺点:如果你在定义中犯了错误(比如你向元组添加了第三个元素)那么错误将在调用站点被标记,而不是在定义。这可能会造成混淆,尤其是当错误发生在深度嵌套的对象中时:
const contexts can neatly solve issues around losing context in inference, but they do have an unfortunate downside: if you make a mistake in the definition (say you add a third element to the tuple) then the error will be flagged at the call site, not at the definition. This may be confusing, especially if the error occurs in a deeply nested object:
constloc=[10,20,30]asconst;// error is really here.panTo(loc);// ~~~ Argument of type 'readonly [10, 20, 30]' is not assignable to// parameter of type 'readonly [number, number]'// Types of property 'length' are incompatible// Type '3' is not assignable to type '2'
constloc=[10,20,30]asconst;// error is really here.panTo(loc);// ~~~ Argument of type 'readonly [10, 20, 30]' is not assignable to// parameter of type 'readonly [number, number]'// Types of property 'length' are incompatible// Type '3' is not assignable to type '2'
这当您从包含一些字符串文字或元组的较大对象中提取常量时,也会出现将值与其上下文分离的问题。例如:
The problem of separating a value from its context also comes up when you factor out a constant from a larger object that contains some string literals or tuples. For example:
typeLanguage='JavaScript'|'TypeScript'|'Python';interfaceGovernedLanguage{language:Language;organization:string;}functioncomplain(language:GovernedLanguage){/* ... */}complain({language:'TypeScript',organization:'Microsoft'});// OKconstts={language:'TypeScript',organization:'Microsoft',};complain(ts);// ~~ Argument of type '{ language: string; organization: string; }'// is not assignable to parameter of type 'GovernedLanguage'// Types of property 'language' are incompatible// Type 'string' is not assignable to type 'Language'
typeLanguage='JavaScript'|'TypeScript'|'Python';interfaceGovernedLanguage{language:Language;organization:string;}functioncomplain(language:GovernedLanguage){/* ... */}complain({language:'TypeScript',organization:'Microsoft'});// OKconstts={language:'TypeScript',organization:'Microsoft',};complain(ts);// ~~ Argument of type '{ language: string; organization: string; }'// is not assignable to parameter of type 'GovernedLanguage'// Types of property 'language' are incompatible// Type 'string' is not assignable to type 'Language'
在对象ts, 的类型language被推断为string。和以前一样,解决方案是添加类型声明 ( const ts: GovernedLanguage = ...) 或使用 const 断言 ( as const)。
In the ts object, the type of language is inferred as string. As before, the solution is to add a type declaration (const ts: GovernedLanguage = ...) or use a const assertion (as const).
什么时候您将回调传递给另一个函数,TypeScript 使用上下文来推断回调的参数类型:
When you pass a callback to another function, TypeScript uses context to infer the parameter types of the callback:
functioncallWithRandomNumbers(fn:(n1:number,n2:number)=>void){fn(Math.random(),Math.random());}callWithRandomNumbers((a,b)=>{a;// Type is numberb;// Type is numberconsole.log(a+b);});
functioncallWithRandomNumbers(fn:(n1:number,n2:number)=>void){fn(Math.random(),Math.random());}callWithRandomNumbers((a,b)=>{a;// Type is numberb;// Type is numberconsole.log(a+b);});
a和的类型b是number根据 的类型声明推断出来的callWithRandom。如果将回调分解为常量,则会丢失该上下文并出现noImplicitAny错误:
The types of a and b are inferred as number because of the type declaration for callWithRandom. If you factor the callback out into a constant, you lose that context and get noImplicitAny errors:
constfn=(a,b)=>{// ~ Parameter 'a' implicitly has an 'any' type// ~ Parameter 'b' implicitly has an 'any' typeconsole.log(a+b);}callWithRandomNumbers(fn);
constfn=(a,b)=>{// ~ Parameter 'a' implicitly has an 'any' type// ~ Parameter 'b' implicitly has an 'any' typeconsole.log(a+b);}callWithRandomNumbers(fn);
解决方案是为参数添加类型注释:
The solution is either to add type annotations to the parameters:
constfn=(a:number,b:number)=>{console.log(a+b);}callWithRandomNumbers(fn);
constfn=(a:number,b:number)=>{console.log(a+b);}callWithRandomNumbers(fn);
或者将类型声明应用于整个函数表达式(如果可用)。请参阅第 12 项。
or to apply a type declaration to the entire function expression if one is available. See Item 12.
请注意在类型推断中如何使用上下文。
Be aware of how context is used in type inference.
如果分解变量引入类型错误,请考虑添加类型声明。
If factoring out a variable introduces a type error, consider adding a type declaration.
如果变量确实是常量,请使用 const 断言 ( as const)。但请注意,这可能会导致在使用时出现错误,而不是定义错误。
If the variable is truly a constant, use a const assertion (as const). But be aware that this may result in errors surfacing at use, rather than definition.
JavaScript从未包含您在 Python、C 或爪哇。多年来,许多图书馆都试图填补这一空白。查询提供的帮助器不仅用于与 DOM 交互,还用于迭代和映射对象和数组。下划线更专注于提供通用的实用功能,而 Lodash 正是基于这一努力而构建的。今天像 Ramda 这样的库继续将函数式编程的思想带入 JavaScript 世界。
JavaScript has never included the sort of standard library you find in Python, C, or Java. Over the years many libraries have tried to fill the gap. jQuery provided helpers not just for interacting with the DOM but also for iterating and mapping over objects and arrays. Underscore focused more on providing general utility functions, and Lodash built on this effort. Today libraries like Ramda continue to bring ideas from functional programming into the JavaScript world.
一些来自这些库的特性,例如map、flatMap、filter和reduce,已经融入了 JavaScript 语言本身。虽然这些结构(以及 Lodash 提供的其他结构)在 JavaScript 中很有用,而且通常比手动循环更可取,但当您将 TypeScript 添加到组合中时,这种优势往往会变得更加不平衡。这是因为它们的类型声明确保类型流经这些构造。使用手卷循环,您自己负责类型。
Some features from these libraries, such as map, flatMap, filter, and reduce, have made it into the JavaScript language itself. While these constructs (and the other ones provided by Lodash) are helpful in JavaScript and often preferable to a hand-rolled loop, this advantage tends to get even more lopsided when you add TypeScript to the mix. This is because their type declarations ensure that types flow through these constructs. With hand-rolled loops, you’re responsible for the types yourself.
为了例如,考虑解析一些 CSV 数据。您可以在纯 JavaScript 中以某种命令式的方式执行此操作:
For example, consider parsing some CSV data. You could do it in plain JavaScript in a somewhat imperative style:
constcsvData="...";constrawRows=csvData.split('\n');constheaders=rawRows[0].split(',');constrows=rawRows.slice(1).map(rowStr=>{constrow={};rowStr.split(',').forEach((val,j)=>{row[headers[j]]=val;});returnrow;});
constcsvData="...";constrawRows=csvData.split('\n');constheaders=rawRows[0].split(',');constrows=rawRows.slice(1).map(rowStr=>{constrow={};rowStr.split(',').forEach((val,j)=>{row[headers[j]]=val;});returnrow;});
更具功能意识的 JavaScript 开发者可能更喜欢使用以下方式构建行对象reduce:
More functionally minded JavaScripters might prefer to build the row objects with reduce:
constrows=rawRows.slice(1).map(rowStr=>rowStr.split(',').reduce((row,val,i)=>(row[headers[i]]=val,row),{}));
constrows=rawRows.slice(1).map(rowStr=>rowStr.split(',').reduce((row,val,i)=>(row[headers[i]]=val,row),{}));
这version 节省了三行(将近 20 个非空白字符!),但根据您的感受,可能会更加神秘。Lodash 的zipObject函数通过“压缩”键和值数组形成一个对象,可以进一步收紧它:
This version saves three lines (almost 20 non-whitespace characters!) but may be more cryptic depending on your sensibilities. Lodash’s zipObject function, which forms an object by “zipping” up a keys and values array, can tighten it even further:
import_from'lodash';constrows=rawRows.slice(1).map(rowStr=>_.zipObject(headers,rowStr.split(',')));
import_from'lodash';constrows=rawRows.slice(1).map(rowStr=>_.zipObject(headers,rowStr.split(',')));
我发现这是最清楚的。但是,为您的项目添加对第三方库的依赖是否值得?如果您没有使用捆绑器并且这样做的开销很大,那么答案可能是否定的。
I find this the clearest of all. But is it worth the cost of adding a dependency on a third-party library to your project? If you’re not using a bundler and the overhead of doing this is significant, then the answer may be “no.”
当您将 TypeScript 添加到组合中时,它开始更倾向于支持 Lodash 解决方案。
When you add TypeScript to the mix, it starts to tip the balance more strongly in favor of the Lodash solution.
CSV 解析器的两个 vanilla JS 版本在 TypeScript 中产生相同的错误:
Both vanilla JS versions of the CSV parser produce the same error in TypeScript:
constrowsA=rawRows.slice(1).map(rowStr=>{constrow={};rowStr.split(',').forEach((val,j)=>{row[headers[j]]=val;// ~~~~~~~~~~~~~~~ No index signature with a parameter of// type 'string' was found on type '{}'});returnrow;});constrowsB=rawRows.slice(1).map(rowStr=>rowStr.split(',').reduce((row,val,i)=>(row[headers[i]]=val,row),// ~~~~~~~~~~~~~~~ No index signature with a parameter of// type 'string' was found on type '{}'{}));
constrowsA=rawRows.slice(1).map(rowStr=>{constrow={};rowStr.split(',').forEach((val,j)=>{row[headers[j]]=val;// ~~~~~~~~~~~~~~~ No index signature with a parameter of// type 'string' was found on type '{}'});returnrow;});constrowsB=rawRows.slice(1).map(rowStr=>rowStr.split(',').reduce((row,val,i)=>(row[headers[i]]=val,row),// ~~~~~~~~~~~~~~~ No index signature with a parameter of// type 'string' was found on type '{}'{}));
每种情况下的解决方案都是为 或{}提供类型注释。{[column: string]: string}Record<string, string>
The solution in each case is to provide a type annotation for {}, either {[column: string]: string} or Record<string, string>.
另一方面,Lodash 版本无需修改即可通过类型检查器:
The Lodash version, on the other hand, passes the type checker without modification:
constrows=rawRows.slice(1).map(rowStr=>_.zipObject(headers,rowStr.split(',')));// Type is _.Dictionary<string>[]
constrows=rawRows.slice(1).map(rowStr=>_.zipObject(headers,rowStr.split(',')));// Type is _.Dictionary<string>[]
Dictionary是一个 Lodash 类型别名。与或Dictionary<string>相同。这里重要的是类型是完全正确的,不需要类型注释。{[key: string]: string}Record<string, string>rows
Dictionary is a Lodash type alias. Dictionary<string> is the same as {[key: string]: string} or Record<string, string>. The important thing here is that the type of rows is exactly correct, no type annotations needed.
随着您的数据处理变得更加精细,这些优势会变得更加明显。例如,假设您有所有 NBA 球队的名单:
These advantages get more pronounced as your data munging gets more elaborate. For example, suppose you have a list of the rosters for all the NBA teams:
interfaceBasketballPlayer{name::::string;team_string;salary_number;}declareconstrosters:{[team_string]:BasketballPlayer[]};
interfaceBasketballPlayer{name:string;team:string;salary:number;}declareconstrosters:{[team:string]:BasketballPlayer[]};
到使用循环构建平面列表,您可以使用concat数组。此代码运行良好但不进行类型检查:
To build a flat list using a loop, you might use concat with an array. This code runs fine but does not type check:
letallPlayers=[];// ~~~~~~~~~~ Variable 'allPlayers' implicitly has type 'any[]'// in some locations where its type cannot be determinedfor(constplayersofObject.values(rosters)){allPlayers=allPlayers.concat(players);// ~~~~~~~~~~ Variable 'allPlayers' implicitly has an 'any[]' type}
letallPlayers=[];// ~~~~~~~~~~ Variable 'allPlayers' implicitly has type 'any[]'// in some locations where its type cannot be determinedfor(constplayersofObject.values(rosters)){allPlayers=allPlayers.concat(players);// ~~~~~~~~~~ Variable 'allPlayers' implicitly has an 'any[]' type}
要修复错误,您需要将类型注释添加到allPlayers:
To fix the error you need to add a type annotation to allPlayers:
letallPlayers:BasketballPlayer[]=[];for(constplayersofObject.values(rosters)){allPlayers=allPlayers.concat(players);// OK}
letallPlayers:BasketballPlayer[]=[];for(constplayersofObject.values(rosters)){allPlayers=allPlayers.concat(players);// OK}
但更好的解决方案是使用Array.prototype.flat:
But a better solution is to use Array.prototype.flat:
constallPlayers=Object.values(rosters).flat();// OK, type is BasketballPlayer[]
constallPlayers=Object.values(rosters).flat();// OK, type is BasketballPlayer[]
该flat方法展平多维数组。它的类型签名类似于T[][] => T[]. 这个版本是最简洁的,不需要类型注释。作为额外的好处,您可以使用constinstead oflet来防止变量将来发生突变allPlayers。
The flat method flattens a multidimensional array. Its type signature is something like T[][] => T[]. This version is the most concise and requires no type annotations. As an added bonus you can use const instead of let to prevent future mutations to the allPlayers variable.
假设您要开始allPlayers并列出按薪水排序的每支球队中收入最高的球员。
Say you want to start with allPlayers and make a list of the highest-paid players on each team ordered by salary.
这是没有 Lodash 的解决方案。它需要一个不使用函数构造的类型注释:
Here’s a solution without Lodash. It requires a type annotation where you don’t use functional constructs:
constteamToPlayers:{[team:string]:BasketballPlayer[]}={};for(constplayerofallPlayers){const{team}=player;teamToPlayers[team]=teamToPlayers[team]||[];teamToPlayers[team].push(player);}for(constplayersofObject.values(teamToPlayers)){players.sort((a,b)=>b.salary-a.salary);}constbestPaid=Object.values(teamToPlayers).map(players=>players[0]);bestPaid.sort((playerA,playerB)=>playerB.salary-playerA.salary);console.log(bestPaid);
constteamToPlayers:{[team:string]:BasketballPlayer[]}={};for(constplayerofallPlayers){const{team}=player;teamToPlayers[team]=teamToPlayers[team]||[];teamToPlayers[team].push(player);}for(constplayersofObject.values(teamToPlayers)){players.sort((a,b)=>b.salary-a.salary);}constbestPaid=Object.values(teamToPlayers).map(players=>players[0]);bestPaid.sort((playerA,playerB)=>playerB.salary-playerA.salary);console.log(bestPaid);
[
{ team: 'GSW', salary: 37457154, name: 'Stephen Curry' },
{ team: 'HOU', salary: 35654150, name: 'Chris Paul' },
{ team: 'LAL', salary: 35654150, name: 'LeBron James' },
{ team: 'OKC', salary: 35654150, name: 'Russell Westbrook' },
{ team: 'DET', salary: 32088932, name: 'Blake Griffin' },
...
][
{ team: 'GSW', salary: 37457154, name: 'Stephen Curry' },
{ team: 'HOU', salary: 35654150, name: 'Chris Paul' },
{ team: 'LAL', salary: 35654150, name: 'LeBron James' },
{ team: 'OKC', salary: 35654150, name: 'Russell Westbrook' },
{ team: 'DET', salary: 32088932, name: 'Blake Griffin' },
...
]
这是 Lodash 的等价物:
Here’s the equivalent with Lodash:
constbestPaid=_(allPlayers).groupBy(player=>player.team).mapValues(players=>_.maxBy(players,p=>p.salary)!).values().sortBy(p=>-p.salary).value()// Type is BasketballPlayer[]
constbestPaid=_(allPlayers).groupBy(player=>player.team).mapValues(players=>_.maxBy(players,p=>p.salary)!).values().sortBy(p=>-p.salary).value()// Type is BasketballPlayer[]
除了长度减半之外,这段代码更清晰,只需要一个非空断言(类型检查器不知道players传递给的数组_.maxBy是非空的)。它使用了“链”,这是 Lodash 中的一个概念,并且允许您以更自然的顺序编写一系列操作的下划线。而不是写:
In addition to being half the length, this code is clearer and requires only a single non-null assertion (the type checker doesn’t know that the players array passed to _.maxBy is non-empty). It makes use of a “chain,” a concept in Lodash and Underscore that lets you write a sequence of operations in a more natural order. Instead of writing:
_.a(_.b(_.c(v)))
_.a(_.b(_.c(v)))
你写:
you write:
_(v).a().b().c().value()
_(v).a().b().c().value()
“包装_(v)”值,然后.value()“解包”它。
The _(v) “wraps” the value, and the .value() “unwraps” it.
您可以检查链中的每个函数调用以查看包装值的类型。它总是正确的。
You can inspect each function call in the chain to see the type of the wrapped value. It’s always correct.
甚至 Lodash 中一些更古怪的简写也可以在 TypeScript 中准确建模。例如,为什么要使用_.map而不是内置的Array.prototype.map?一个原因是,您可以传入属性名称,而不是传入回调。这些调用都产生相同的结果:
Even some of the quirkier shorthands in Lodash can be modeled accurately in TypeScript. For instance, why would you want to use _.map instead of the built-in Array.prototype.map? One reason is that instead of passing in a callback you can pass in the name of a property. These calls all produce the same result:
constnamesA=allPlayers.map(player=>player.name)// Type is string[]constnamesB=_.map(allPlayers,player=>player.name)// Type is string[]constnamesC=_.map(allPlayers,'name');// Type is string[]
constnamesA=allPlayers.map(player=>player.name)// Type is string[]constnamesB=_.map(allPlayers,player=>player.name)// Type is string[]constnamesC=_.map(allPlayers,'name');// Type is string[]
这证明了 TypeScript 的类型系统的复杂性,它可以准确地模拟这样的构造,但它自然地脱离了字符串文字类型和索引类型的组合(参见Item 14)。如果你习惯了 C++ 或Java,这种类型推断感觉很神奇!
It’s a testament to the sophistication of TypeScript’s type system that it can model a construct like this accurately, but it naturally falls out of the combination of string literal types and index types (see Item 14). If you’re used to C++ or Java, this sort of type inference can feel quite magical!
constsalaries=_.map(allPlayers,'salary');// Type is number[]constteams=_.map(allPlayers,'team');// Type is string[]constmix=_.map(allPlayers,Math.random()<0.5?'name':'salary');// Type is (string | number)[]
constsalaries=_.map(allPlayers,'salary');// Type is number[]constteams=_.map(allPlayers,'team');// Type is string[]constmix=_.map(allPlayers,Math.random()<0.5?'name':'salary');// Type is (string | number)[]
类型在内置函数构造和 Lodash 等库中的构造如此流畅并非巧合。通过避免突变并从每次调用中返回新值,它们也能够生成新类型(条目 20)。在很大程度上,TypeScript 的发展一直受到对 JavaScript 库的行为进行准确建模的尝试的推动。充分利用所有这些工作并使用它们!
It’s not a coincidence that types flow so well through built-in functional constructs and those in libraries like Lodash. By avoiding mutation and returning new values from every call, they are able to produce new types as well (Item 20). And to a large extent, the development of TypeScript has been driven by an attempt to accurately model the behavior of JavaScript libraries in the wild. Take advantage of all this work and use them!
给我看你的流程图并隐藏你的表格,我将继续感到困惑。给我看你的表格,我通常不需要你的流程图;他们会很明显。
弗雷德·布鲁克斯,人月神话
Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious.
Fred Brooks, The Mythical Man Month
这Fred Brooks 的话中的语言已经过时,但情感仍然是真实的:如果您看不到代码运行的数据或数据类型,则很难理解代码。这是类型系统的一大优势:通过写出类型,您可以让代码的读者看到它们。这使您的代码易于理解。
The language in Fred Brooks’s quote is dated, but the sentiment remains true: code is difficult to understand if you can’t see the data or data types on which it operates. This is one of the great advantages of a type system: by writing out types, you make them visible to readers of your code. And this makes your code understandable.
其他章节涵盖了 TypeScript 类型的具体细节:使用它们、推断它们以及使用它们编写声明。本章讨论类型本身的设计。本章中的示例都是用 TypeScript 编写的,但大多数想法的适用范围更广。
Other chapters cover the nuts and bolts of TypeScript types: using them, inferring them, and writing declarations with them. This chapter discusses the design of the types themselves. The examples in this chapter are all written with TypeScript in mind, but most of the ideas are more broadly applicable.
如果你的类型写得很好,那么幸运的话你的流程图也会很明显。
If you write your types well, then with any luck your flowcharts will be obvious, too.
如果你的类型设计得很好,你的代码应该很容易编写。但是,如果你设计的类型很糟糕,再多的聪明才智或文档也救不了你。您的代码将变得混乱且容易出错。
If you design your types well, your code should be straightforward to write. But if you design your types poorly, no amount of cleverness or documentation will save you. Your code will be confusing and bug prone.
有效类型设计的关键是制作只能代表有效状态的类型。这篇文章通过几个例子来说明这可能会出错,并向您展示如何修复它们。
A key to effective type design is crafting types that can only represent a valid state. This item walks through a few examples of how this can go wrong and shows you how to fix them.
假设您正在构建一个 Web 应用程序,它允许您选择一个页面、加载该页面的内容,然后显示它。你可以这样写状态:
Suppose you’re building a web application that lets you select a page, loads the content of that page, and then displays it. You might write the state like this:
interfaceState{pageText:string;isLoading:boolean;error?:string;}
interfaceState{pageText:string;isLoading:boolean;error?:string;}
当您编写代码来呈现页面时,您需要考虑所有这些字段:
When you write your code to render the page, you need to consider all of these fields:
functionrenderPage(state:State){if(state.error){return`Error! Unable to load${currentPage}:${state.error}`;}elseif(state.isLoading){return`Loading${currentPage}...`;}return`<h1>${currentPage}</h1>\n${state.pageText}`;}
functionrenderPage(state:State){if(state.error){return`Error! Unable to load${currentPage}:${state.error}`;}elseif(state.isLoading){return`Loading${currentPage}...`;}return`<h1>${currentPage}</h1>\n${state.pageText}`;}
这是对的吗?如果isLoading和error都设置了怎么办?那是什么意思?是显示加载信息好还是显示错误信息好?很难说!没有足够的可用信息。
Is this right, though? What if isLoading and error are both set? What would that mean? Is it better to display the loading message or the error message? It’s hard to say! There’s not enough information available.
或者,如果您正在编写changePage函数怎么办?这是一个尝试:
Or what if you’re writing a changePage function? Here’s an attempt:
asyncfunctionchangePage(state:State,newPage:string){state.isLoading=true;try{constresponse=awaitfetch(getUrlForPage(newPage));if(!response.ok){thrownewError(`Unable to load${newPage}:${response.statusText}`);}consttext=awaitresponse.text();state.isLoading=false;state.pageText=text;}catch(e){state.error=''+e;}}
asyncfunctionchangePage(state:State,newPage:string){state.isLoading=true;try{constresponse=awaitfetch(getUrlForPage(newPage));if(!response.ok){thrownewError(`Unable to load${newPage}:${response.statusText}`);}consttext=awaitresponse.text();state.isLoading=false;state.pageText=text;}catch(e){state.error=''+e;}}
这有很多问题!这里有一些:
There are many problems with this! Here are a few:
我们忘记在错误情况下设置state.isLoading为。false
We forgot to set state.isLoading to false in the error case.
我们没有清除state.error,所以如果之前的请求失败,那么您将继续看到该错误消息而不是加载消息。
We didn’t clear out state.error, so if the previous request failed, then you’ll keep seeing that error message instead of a loading message.
如果用户在页面加载时再次更改页面,谁知道会发生什么。他们可能会看到一个新页面,然后是一个错误,或者是第一页而不是第二页,具体取决于响应返回的顺序。
If the user changes pages again while the page is loading, who knows what will happen. They might see a new page and then an error, or the first page and not the second depending on the order in which the responses come back.
问题是状态包含的信息太少(哪个请求失败?哪个正在加载?)和太多:类型State允许同时设置isLoading和,即使这表示无效状态。error这使得两者render()都changePage()无法很好地实施。
The problem is that the state includes both too little information (which request failed? which is loading?) and too much: the State type allows both isLoading and error to be set, even though this represents an invalid state. This makes both render() and changePage() impossible to implement well.
这是表示应用程序状态的更好方法:
Here’s a better way to represent the application state:
interfaceRequestPending{state:'pending';}interfaceRequestError{state:'error';error:string;}interfaceRequestSuccess{state:'ok';pageText:string;}typeRequestState=RequestPending|RequestError|RequestSuccess;interfaceState{currentPage:string;requests:{[page:string]:RequestState};}
interfaceRequestPending{state:'pending';}interfaceRequestError{state:'error';error:string;}interfaceRequestSuccess{state:'ok';pageText:string;}typeRequestState=RequestPending|RequestError|RequestSuccess;interfaceState{currentPage:string;requests:{[page:string]:RequestState};}
这使用标记联合(也称为“区分联合”)来显式建模网络请求可能处于的不同状态。这个版本的状态要长三到四倍,但它具有不承认的巨大优势无效状态。当前页面是明确建模的,就像您发出的每个请求的状态一样。因此,renderPage和changePage功能很容易实现:
This uses a tagged union (also known as a “discriminated union”) to explicitly model the different states that a network request can be in. This version of the state is three to four times longer, but it has the enormous advantage of not admitting invalid states. The current page is modeled explicitly, as is the state of every request that you issue. As a result, the renderPage and changePage functions are easy to implement:
functionrenderPage(state:State){const{currentPage}=state;constrequestState=state.requests[currentPage];switch(requestState.state){case'pending':return`Loading${currentPage}...`;case'error':return`Error! Unable to load${currentPage}:${requestState.error}`;case'ok':return`<h1>${currentPage}</h1>\n${requestState.pageText}`;}}asyncfunctionchangePage(state:State,newPage:string){state.requests[newPage]={state:'pending'};state.currentPage=newPage;try{constresponse=awaitfetch(getUrlForPage(newPage));if(!response.ok){thrownewError(`Unable to load${newPage}:${response.statusText}`);}constpageText=awaitresponse.text();state.requests[newPage]={state:'ok',pageText};}catch(e){state.requests[newPage]={state:'error',error:''+e};}}
functionrenderPage(state:State){const{currentPage}=state;constrequestState=state.requests[currentPage];switch(requestState.state){case'pending':return`Loading${currentPage}...`;case'error':return`Error! Unable to load${currentPage}:${requestState.error}`;case'ok':return`<h1>${currentPage}</h1>\n${requestState.pageText}`;}}asyncfunctionchangePage(state:State,newPage:string){state.requests[newPage]={state:'pending'};state.currentPage=newPage;try{constresponse=awaitfetch(getUrlForPage(newPage));if(!response.ok){thrownewError(`Unable to load${newPage}:${response.statusText}`);}constpageText=awaitresponse.text();state.requests[newPage]={state:'ok',pageText};}catch(e){state.requests[newPage]={state:'error',error:''+e};}}
第一个实现的歧义完全消失了:当前页面是什么一目了然,每个请求都恰好处于一个状态。如果用户在发出请求后更改页面,那也没有问题。旧请求仍会完成,但不会影响 UI。
The ambiguity from the first implementation is entirely gone: it’s clear what the current page is, and every request is in exactly one state. If the user changes the page after a request has been issued, that’s no problem either. The old request still completes, but it doesn’t affect the UI.
举一个更简单但更可怕的例子,想想 2009 年 6 月 1 日法航 447 号航班的命运,这是一架空中客车 330,它在大西洋上空失踪。在影响飞机的物理控制面之前通过计算机系统。事故发生后,人们提出了许多关于依靠计算机做出这种生死攸关的决定是否明智的问题。两年后,当黑匣子被找回时,它们揭示了导致坠机的许多因素。但关键是糟糕的状态设计。
For a simpler but more dire example, consider the fate of Air France Flight 447, an Airbus 330 that disappeared over the Atlantic on June 1, 2009. The Airbus was a fly-by-wire aircraft, meaning that the pilots’ control inputs went through a computer system before affecting the physical control surfaces of the plane. In the wake of the crash there were many questions raised about the wisdom of relying on computers to make such life-and-death decisions. Two years later when the black box recorders were recovered, they revealed many factors that led to the crash. But a key one was bad state design.
空中客车 330 的驾驶舱有一套单独的飞行员和副驾驶控制装置。“侧杆”控制攻角。向后拉会使飞机爬升,而向前推会使飞机俯冲。空中客车 330 使用了一种称为“双输入”模式的系统,让两个侧杆独立移动。以下是您可以如何在 TypeScript 中对其状态进行建模:
The cockpit of the Airbus 330 had a separate set of controls for the pilot and copilot. The “side sticks” controlled the angle of attack. Pulling back would send the airplane into a climb, while pushing forward would make it dive. The Airbus 330 used a system called “dual input” mode, which let the two side sticks move independently. Here’s how you might model its state in TypeScript:
interfaceCockpitControls{/** Angle of the left side stick in degrees, 0 = neutral, + = forward */leftSideStick:number;/** Angle of the right side stick in degrees, 0 = neutral, + = forward */rightSideStick:number;}
interfaceCockpitControls{/** Angle of the left side stick in degrees, 0 = neutral, + = forward */leftSideStick:number;/** Angle of the right side stick in degrees, 0 = neutral, + = forward */rightSideStick:number;}
假设你得到了这个数据结构,并被要求编写一个getStickSetting函数来计算当前的摇杆设置。你会怎么做?
Suppose you were given this data structure and asked to write a getStickSetting function that computed the current stick setting. How would you do it?
一种方法是假设飞行员(坐在左边)处于控制之中:
One way would be to assume that the pilot (who sits on the left) is in control:
functiongetStickSetting(controls:CockpitControls){returncontrols.leftSideStick;}
functiongetStickSetting(controls:CockpitControls){returncontrols.leftSideStick;}
但是如果副驾驶已经控制了呢?也许你应该使用远离零的那根棍子:
But what if the copilot has taken control? Maybe you should use whichever stick is away from zero:
functiongetStickSetting(controls:CockpitControls){const{leftSideStick,rightSideStick}=controls;if(leftSideStick===0){returnrightSideStick;}returnleftSideStick;}
functiongetStickSetting(controls:CockpitControls){const{leftSideStick,rightSideStick}=controls;if(leftSideStick===0){returnrightSideStick;}returnleftSideStick;}
但是这个实现有一个问题:如果右边的设置是中性的,我们只能自信地返回左边的设置。所以你应该检查一下:
But there’s a problem with this implementation: we can only be confident returning the left setting if the right one is neutral. So you should check for that:
functiongetStickSetting(controls:CockpitControls){const{leftSideStick,rightSideStick}=controls;if(leftSideStick===0){returnrightSideStick;}elseif(rightSideStick===0){returnleftSideStick;}// ???}
functiongetStickSetting(controls:CockpitControls){const{leftSideStick,rightSideStick}=controls;if(leftSideStick===0){returnrightSideStick;}elseif(rightSideStick===0){returnleftSideStick;}// ???}
如果它们都不为零,你会怎么做?希望它们大致相同,在这种情况下,您可以对它们进行平均:
What do you do if they’re both non-zero? Hopefully they’re about the same, in which case you could just average them:
functiongetStickSetting(controls:CockpitControls){const{leftSideStick,rightSideStick}=controls;if(leftSideStick===0){returnrightSideStick;}elseif(rightSideStick===0){returnleftSideStick;}if(Math.abs(leftSideStick-rightSideStick)<5){return(leftSideStick+rightSideStick)/2;}// ???}
functiongetStickSetting(controls:CockpitControls){const{leftSideStick,rightSideStick}=controls;if(leftSideStick===0){returnrightSideStick;}elseif(rightSideStick===0){returnleftSideStick;}if(Math.abs(leftSideStick-rightSideStick)<5){return(leftSideStick+rightSideStick)/2;}// ???}
但如果他们不是呢?你能抛出一个错误吗?不完全是:副翼需要设置成某个角度!
But what if they’re not? Can you throw an error? Not really: the ailerons need to be set at some angle!
在法航 447 上,当飞机进入风暴时,副驾驶默默地拉回他的侧杆。它获得了高度但最终失去了速度并进入了失速状态,在这种情况下飞机移动太慢而无法有效产生升力。它开始下降。
On Air France 447, the copilot silently pulled back on his side stick as the plane entered a storm. It gained altitude but eventually lost speed and entered a stall, a condition in which the plane is moving too slowly to effectively generate lift. It began to drop.
为了摆脱失速,飞行员接受了向前推动控制装置以使飞机俯冲并恢复速度的训练。这正是飞行员所做的。但副驾驶仍在默默地向后拉他的侧杆。空客函数看起来像这样:
To escape a stall, pilots are trained to push the controls forward to make the plane dive and regain speed. This is exactly what the pilot did. But the copilot was still silently pulling back on his side stick. And the Airbus function looked like this:
functiongetStickSetting(controls:CockpitControls){return(controls.leftSideStick+controls.rightSideStick)/2;}
functiongetStickSetting(controls:CockpitControls){return(controls.leftSideStick+controls.rightSideStick)/2;}
尽管飞行员将操纵杆完全向前推,但它平均为零。他不知道为什么飞机不俯冲。当副驾驶透露他的所作所为时,飞机已经失去太多高度无法恢复,坠入大海,机上 228 人全部遇难。
Even though the pilot pushed the stick fully forward, it averaged out to nothing. He had no idea why the plane wasn’t diving. By the time the copilot revealed what he’d done, the plane had lost too much altitude to recover and it crashed into the ocean, killing all 228 people on board.
所有这一切的要点是没有好的方法来实现getStickSetting给定的输入!该功能已设置为失败。在大多数飞机上,两组控制装置是机械连接的。如果副驾驶向后拉,飞行员的控制装置也会向后拉。这些控件的状态很容易表达:
The point of all this is that there is no good way to implement getStickSetting given that input! The function has been set up to fail. In most planes the two sets of controls are mechanically connected. If the copilot pulls back, the pilot’s controls will also pull back. The state of these controls is simple to express:
interfaceCockpitControls{/** Angle of the stick in degrees, 0 = neutral, + = forward */stickAngle:number;}
interfaceCockpitControls{/** Angle of the stick in degrees, 0 = neutral, + = forward */stickAngle:number;}
和现在,正如本章开头引用 Fred Brooks 的话,我们的流程图是显而易见的。getStickSetting你根本不需要一个函数。
And now, as in the Fred Brooks quote from the start of the chapter, our flowcharts are obvious. You don’t need a getStickSetting function at all.
在设计类型时,请注意考虑要包括哪些值以及要排除哪些值。如果您只允许代表有效状态的值,您的代码将更容易编写,并且 TypeScript 将更容易检查它。这是一个非常普遍的原则,本章的其他几项将涵盖它的具体表现形式。
As you design your types, take care to think about which values you are including and which you are excluding. If you only allow values that represent valid states, your code will be easier to write and TypeScript will have an easier time checking it. This is a very general principle, and several of the other items in this chapter will cover specific manifestations of it.
这这个想法被称为健壮性原则或Postel 定律,以 Jon Postel 的名字命名,他是在 TCP 的上下文中写的:
This idea is known as the robustness principle or Postel’s Law, after Jon Postel, who wrote it in the context of TCP:
TCP 实现应遵循健壮性的一般原则:在您所做的事情上保持保守,在您从他人那里接受的事情上保持自由。
TCP implementations should follow a general principle of robustness: be conservative in what you do, be liberal in what you accept from others.
类似的规则适用于功能合同。您的函数接受的输入内容很广泛是很好的,但它们通常应该更具体地说明它们作为输出产生的内容。
A similar rule applies to the contracts for functions. It’s fine for your functions to be broad in what they accept as inputs, but they should generally be more specific in what they produce as outputs.
例如,3D 映射 API 可能提供一种定位相机和计算边界框视口的方法:
As an example, a 3D mapping API might provide a way to position the camera and to calculate a viewport for a bounding box:
declarefunctionsetCamera(camera:CameraOptions):void;declarefunctionviewportForBounds(bounds:LngLatBounds):CameraOptions;
declarefunctionsetCamera(camera:CameraOptions):void;declarefunctionviewportForBounds(bounds:LngLatBounds):CameraOptions;
可以直接将的结果viewportForBounds传递给setCamera定位相机,很方便。
It is convenient that the result of viewportForBounds can be passed directly to setCamera to position the camera.
让我们看看这些类型的定义:
Let’s look at the definitions of these types:
interfaceCameraOptions{center?:::::::::LngLat;zoom?_number;bearing?_number;pitch?_number;}typeLngLat={lng_number;lat_number;}|{lon_number;lat_number;}|[number,number];
interfaceCameraOptions{center?:LngLat;zoom?:number;bearing?:number;pitch?:number;}typeLngLat={lng:number;lat:number;}|{lon:number;lat:number;}|[number,number];
中的字段CameraOptions都是可选的,因为您可能只想设置中心或缩放而不更改方位或间距。该LngLat类型在接受什么方面也很自由:如果您确信顺序正确,setCamera您可以传入一个{lng, lat}对象、一个{lon, lat}对象或一对对象。[lng, lat]这些调整使函数易于调用。
The fields in CameraOptions are all optional because you might want to set just the center or zoom without changing the bearing or pitch. The LngLat type also makes setCamera liberal in what it accepts: you can pass in a {lng, lat} object, a {lon, lat} object, or a [lng, lat] pair if you’re confident you got the order right. These accommodations make the function easy to call.
该viewportForBounds函数采用另一种“自由”类型:
The viewportForBounds function takes in another “liberal” type:
typeLngLatBounds={northeast:LngLat,southwest:LngLat}|[LngLat,LngLat]|[number,number,number,number];
typeLngLatBounds={northeast:LngLat,southwest:LngLat}|[LngLat,LngLat]|[number,number,number,number];
您可以使用命名角点、一对纬度/经度或四元组指定边界(如果您确信顺序正确)。由于LngLat已经容纳了三种形式,因此 . 的可能形式不少于 19 种LngLatBounds。确实自由!
You can specify the bounds either using named corners, a pair of lat/lngs, or a four-tuple if you’re confident you got the order right. Since LngLat already accommodates three forms, there are no fewer than 19 possible forms for LngLatBounds. Liberal indeed!
现在让我们编写一个函数来调整视口以适应 GeoJSON 功能并将新视口存储在 URL 中(有关 的定义calculateBoundingBox,请参阅项目 31):
Now let’s write a function that adjusts the viewport to accommodate a GeoJSON Feature and stores the new viewport in the URL (for a definition of calculateBoundingBox, see Item 31):
functionfocusOnFeature(f:Feature){constbounds=calculateBoundingBox(f);constcamera=viewportForBounds(bounds);setCamera(camera);const{center:{lat,lng},zoom}=camera;// ~~~ Property 'lat' does not exist on type ...// ~~~ Property 'lng' does not exist on type ...zoom;// Type is number | undefinedwindow.location.search=`?v=@${lat},${lng}z${zoom}`;}
functionfocusOnFeature(f:Feature){constbounds=calculateBoundingBox(f);constcamera=viewportForBounds(bounds);setCamera(camera);const{center:{lat,lng},zoom}=camera;// ~~~ Property 'lat' does not exist on type ...// ~~~ Property 'lng' does not exist on type ...zoom;// Type is number | undefinedwindow.location.search=`?v=@${lat},${lng}z${zoom}`;}
哎呀!只有zoom属性存在,但其类型被推断为number|undefined,这也是有问题的。问题在于类型声明viewportForBounds表明它不仅在它接受的内容上而且在它产生的内容上都是自由的。使用结果的唯一类型安全的方法camera是为联合类型的每个组件引入一个代码分支(条目 22)。
Whoops! Only the zoom property exists, but its type is inferred as number|undefined, which is also problematic. The issue is that the type declaration for viewportForBounds indicates that it is liberal not just in what it accepts but also in what it produces. The only type-safe way to use the camera result is to introduce a code branch for each component of the union type (Item 22).
这具有大量可选属性和联合类型的返回类型很难viewportForBounds使用。其广泛的参数类型很方便,但其广泛的返回类型却不方便。一个更方便的 API 会对其产生的内容有严格的要求。
The return type with lots of optional properties and union types makes viewportForBounds difficult to use. Its broad parameter type is convenient, but its broad return type is not. A more convenient API would be strict in what it produces.
一种方法是区分坐标的规范格式。按照 JavaScript 区分“数组”和“类数组”(条目 16)的惯例,您可以区分LngLat和LngLatLike。您还可以区分完全定义的Camera类型和接受的部分版本setCamera:
One way to do this is to distinguish a canonical format for coordinates. Following JavaScript’s convention of distinguishing “Array” and “Array-like” (Item 16), you can draw a distinction between LngLat and LngLatLike. You can also distinguish between a fully defined Camera type and the partial version accepted by setCamera:
interfaceLngLat{lng::::number;lat_number;};typeLngLatLike=LngLat|{lon_number;lat_number;}|[number,number];interfaceCamera{center::::::::LngLat;zoom_number;bearing_number;pitch_number;}interfaceCameraOptionsextendsOmit<Partial<Camera>,'center'>{center?_LngLatLike;}typeLngLatBounds={northeast_LngLatLike,southwest_LngLatLike}|[LngLatLike,LngLatLike]|[number,number,number,number];declarefunctionsetCamera(camera:CameraOptions):void;declarefunctionviewportForBounds(bounds:LngLatBounds):Camera;
interfaceLngLat{lng:number;lat:number;};typeLngLatLike=LngLat|{lon:number;lat:number;}|[number,number];interfaceCamera{center:LngLat;zoom:number;bearing:number;pitch:number;}interfaceCameraOptionsextendsOmit<Partial<Camera>,'center'>{center?:LngLatLike;}typeLngLatBounds={northeast:LngLatLike,southwest:LngLatLike}|[LngLatLike,LngLatLike]|[number,number,number,number];declarefunctionsetCamera(camera:CameraOptions):void;declarefunctionviewportForBounds(bounds:LngLatBounds):Camera;
松散CameraOptions类型适应更严格的Camera类型(第 14 项)。
The loose CameraOptions type adapts the stricter Camera type (Item 14).
用作Partial<Camera>参数类型在setCamera此处不起作用,因为您确实希望允许LngLatLike该center属性的对象。你不能写“ CameraOptions extends Partial<Camera>”,因为LngLatLike它是 的超集LngLat,而不是子集(第 7 项)。如果这看起来太复杂,您也可以以一些重复为代价明确地写出类型:
Using Partial<Camera> as the parameter type in setCamera would not work here since you do want to allow LngLatLike objects for the center property. And you can’t write "CameraOptions extends Partial<Camera>" since LngLatLike is a superset of LngLat, not a subset (Item 7). If this seems too complicated, you could also write the type out explicitly at the cost of some repetition:
interfaceCameraOptions{center?::::LngLatLike;zoom?_number;bearing?_number;pitch?_number;}
interfaceCameraOptions{center?:LngLatLike;zoom?:number;bearing?:number;pitch?:number;}
在任何一种情况下,使用这些新的类型声明,focusOnFeature函数都会通过类型检查器:
In either case, with these new type declarations the focusOnFeature function passes the type checker:
functionfocusOnFeature(f:Feature){constbounds=calculateBoundingBox(f);constcamera=viewportForBounds(bounds);setCamera(camera);const{center:{lat,lng},zoom}=camera;// OKzoom;// Type is numberwindow.location.search=`?v=@${lat},${lng}z${zoom}`;}
functionfocusOnFeature(f:Feature){constbounds=calculateBoundingBox(f);constcamera=viewportForBounds(bounds);setCamera(camera);const{center:{lat,lng},zoom}=camera;// OKzoom;// Type is numberwindow.location.search=`?v=@${lat},${lng}z${zoom}`;}
这次的类型zoom是number,而不是number|undefined。该viewportForBounds功能现在更易于使用。如果有任何其他函数产生边界,您还需要引入规范形式以及LngLatBounds和之间的区别LngLatBoundsLike。
This time the type of zoom is number, rather than number|undefined. The viewportForBounds function is now much easier to use. If there were any other functions that produced bounds, you would also need to introduce a canonical form and a distinction between LngLatBounds and LngLatBoundsLike.
允许 19 种可能形式的边界框是一个好的设计吗?也许不是。但是,如果您正在为执行此操作的库编写类型声明,则需要对其行为进行建模。只是没有 19 种返回类型!
Is allowing 19 possible forms of bounding box a good design? Perhaps not. But if you’re writing type declarations for a library that does this, you need to model its behavior. Just don’t have 19 return types!
输入类型往往比输出类型更广泛。可选属性和联合类型在参数类型中比返回类型更常见。
Input types tend to be broader than output types. Optional properties and union types are more common in parameter types than return types.
To reuse types between parameters and return types, introduce a canonical form (for return types) and a looser form (for parameters).
/*** Returns a string with the foreground color.* Takes zero or one arguments. With no arguments, returns the* standard foreground color. With one argument, returns the foreground color* for a particular page.*/functiongetForegroundColor(page?::::::::string){returnpage==='login'?{r_127,g_127,b_127}:{r_0,g_0,b_0};}
/*** Returns a string with the foreground color.* Takes zero or one arguments. With no arguments, returns the* standard foreground color. With one argument, returns the foreground color* for a particular page.*/functiongetForegroundColor(page?:string){returnpage==='login'?{r:127,g:127,b:127}:{r:0,g:0,b:0};}
代码和评论不一致!没有更多的上下文,很难说哪个是对的,但显然有些地方不对劲。正如我的一位教授过去常说的那样,“当你的代码和你的评论不一致时,它们都是错误的!”
The code and the comment disagree! Without more context it’s hard to say which is right, but something is clearly amiss. As a professor of mine used to say, “when your code and your comments disagree, they’re both wrong!”
让我们假设代码表示所需的行为。这个评论有几个问题:
Let’s assume that the code represents the desired behavior. There are a few issues with this comment:
string它说该函数在实际返回对象时返回颜色作为 a {r, g, b}。
It says that the function returns the color as a string when it actually returns an {r, g, b} object.
它解释了该函数接受零个或一个参数,这从类型签名中已经很清楚了。
It explains that the function takes zero or one arguments, which is already clear from the type signature.
多余的罗嗦:注释比函数声明和实现还长!
It’s needlessly wordy: the comment is longer than the function declaration and implementation!
TypeScript 的类型注释系统旨在紧凑、描述性和可读性。它的开发人员是具有数十年经验的语言专家。几乎可以肯定,它是一种比散文更好的表达函数输入和输出类型的方法!
TypeScript’s type annotation system is designed to be compact, descriptive, and readable. Its developers are language experts with decades of experience. It’s almost certainly a better way to express the types of your function’s inputs and outputs than your prose!
而且因为类型注释由 TypeScript 编译器检查,所以它们永远不会与实现不同步。可能getForegroundColor用于返回一个字符串,但后来更改为返回一个对象。进行更改的人可能忘记更新长评论。
And because your type annotations are checked by the TypeScript compiler, they’ll never get out of sync with the implementation. Perhaps getForegroundColor used to return a string but was later changed to return an object. The person who made the change might have forgotten to update the long comment.
除非被迫,否则什么都不会保持同步。有了类型注解,TypeScript 的类型检查器就是这样的力量!如果您将类型信息放在注释中而不是文档中,您将大大增加您的信心,相信它会随着代码的发展而保持正确。
Nothing stays in sync unless it’s forced to. With type annotations, TypeScript’s type checker is that force! If you put type information in annotations and not in documentation, you greatly increase your confidence that it will remain correct as the code evolves.
更好的评论可能是这样的:
A better comment might look like this:
/** Get the foreground color for the application or a specific page. */functiongetForegroundColor(page?:string):Color{// ...}
/** Get the foreground color for the application or a specific page. */functiongetForegroundColor(page?:string):Color{// ...}
如果要描述特定参数,请使用@paramJSDoc 注释。有关更多信息,请参见第 48 项。
If you want to describe a particular parameter, use an @param JSDoc annotation. See Item 48 for more on this.
关于缺乏突变的评论也值得怀疑。不要只说你不修改一个参数:
Comments about a lack of mutation are also suspect. Don’t just say that you don’t modify a parameter:
/** Does not modify nums */functionsort(nums:number[]){/* ... */}
/** Does not modify nums */functionsort(nums:number[]){/* ... */}
相反,声明它readonly(第 17 项)并让 TypeScript 执行契约:
Instead, declare it readonly (Item 17) and let TypeScript enforce the contract:
functionsort(nums:readonlynumber[]){/* ... */}
functionsort(nums:readonlynumber[]){/* ... */}
什么是对于注释也是如此,对于变量名也是如此。ageNum避免将类型放入其中:与其命名变量,不如命名它age并确保它确实是一个number.
What’s true for comments is also true for variable names. Avoid putting types in them: rather than naming a variable ageNum, name it age and make sure it’s really a number.
一个例外是带有单位的数字。如果不清楚单位是什么,您可能希望将它们包含在变量或属性名称中。例如,timeMs是一个比 . 更清晰的名称time,并且temperatureC是一个比temperature. 第 37 项描述了“品牌”,它为建模单元提供了一种类型更安全的方法。
An exception to this is for numbers with units. If it’s not clear what the units are, you may want to include them in a variable or property name. For instance, timeMs is a much clearer name than just time, and temperatureC is a much clearer name than temperature. Item 37 describes “brands,” which provide a more type-safe approach to modeling units.
避免在注释和变量名中重复类型信息。在最好的情况下,它是类型声明的重复,在最坏的情况下,它会导致信息冲突。
Avoid repeating type information in comments and variable names. In the best case it is duplicative of type declarations, and in the worst it will lead to conflicting information.
如果类型不明确(例如,timeMs或temperatureC),请考虑在变量名称中包含单位。
Consider including units in variable names if they aren’t clear from the type (e.g., timeMs or temperatureC).
什么时候当您第一次打开 时strictNullChecks,似乎您必须在整个代码中添加大量 if 语句检查null和undefined值。这通常是因为空值和非空值之间的关系是隐式的:当变量 A 为非空时,您知道变量 B 也为非空,反之亦然。这些隐式关系让代码的人类读者和类型检查器都感到困惑。
When you first turn on strictNullChecks, it may seem as though you have to add scores of if statements checking for null and undefined values throughout your code. This is often because the relationships between null and non-null values are implicit: when variable A is non-null, you know that variable B is also non-null and vice versa. These implicit relationships are confusing both for human readers of your code and for the type checker.
当值完全为空或完全非空时,而不是混合时,它们更容易使用。您可以通过将空值推到结构的周边来对其进行建模。
Values are easier to work with when they’re either completely null or completely non-null, rather than a mix. You can model this by pushing the null values out to the perimeter of your structures.
假设您要计算数字列表的最小值和最大值。我们将其称为“范围”。这是一个尝试:
Suppose you want to calculate the min and max of a list of numbers. We’ll call this the “extent.” Here’s an attempt:
functionextent(nums:number[]){letmin,max;for(constnumofnums){if(!min){min=num;max=num;}else{min=Math.min(min,num);max=Math.max(max,num);}}return[min,max];}
functionextent(nums:number[]){letmin,max;for(constnumofnums){if(!min){min=num;max=num;}else{min=Math.min(min,num);max=Math.max(max,num);}}return[min,max];}
代码类型检查(没有strictNullChecks)并且推断返回类型为number[],这看起来不错。但它有一个错误和设计缺陷:
The code type checks (without strictNullChecks) and has an inferred return type of number[], which seems fine. But it has a bug and a design flaw:
如果最小值或最大值为零,它可能会被覆盖。例如,extent([0, 1, 2])将返回[1, 2]而不是[0, 2]。
If the min or max is zero, it may get overridden. For example, extent([0, 1, 2]) will return [1, 2] rather than [0, 2].
如果nums数组为空,函数将返回[undefined, undefined]。这种具有多个undefineds 的对象对于客户来说很难使用,并且正是该项目不鼓励使用的类型。通过阅读源代码,我们知道min和max要么都存在,undefined要么都不是,但该信息并未在类型系统中表示。
If the nums array is empty, the function will return [undefined, undefined]. This sort of object with several undefineds will be difficult for clients to work with and is exactly the sort of type that this item discourages. We know from reading the source code that min and max will either both be undefined or neither, but that information isn’t represented in the type system.
打开strictNullChecks使这两个问题更加明显:
Turning on strictNullChecks makes both of these issues more apparent:
functionextent(nums:number[]){letmin,max;for(constnumofnums){if(!min){min=num;max=num;}else{min=Math.min(min,num);max=Math.max(max,num);// ~~~ Argument of type 'number | undefined' is not// assignable to parameter of type 'number'}}return[min,max];}
functionextent(nums:number[]){letmin,max;for(constnumofnums){if(!min){min=num;max=num;}else{min=Math.min(min,num);max=Math.max(max,num);// ~~~ Argument of type 'number | undefined' is not// assignable to parameter of type 'number'}}return[min,max];}
的返回类型extent现在被推断为(number | undefined)[],这使得设计缺陷更加明显。无论您在哪里调用,这都可能表现为类型错误extent:
The return type of extent is now inferred as (number | undefined)[], which makes the design flaw more apparent. This is likely to manifest as a type error wherever you call extent:
const[min,max]=extent([0,1,2]);constspan=max-min;// ~~~ ~~~ Object is possibly 'undefined'
const[min,max]=extent([0,1,2]);constspan=max-min;// ~~~ ~~~ Object is possibly 'undefined'
的实现中extent出现错误是因为您已将但不是的undefined值排除在外。两者一起初始化,但此信息不存在于类型系统中。您也可以通过添加对 的检查来使其消失,但这会使错误加倍。minmaxmax
The error in the implementation of extent comes about because you’ve excluded undefined as a value for min but not max. The two are initialized together, but this information isn’t present in the type system. You could make it go away by adding a check for max, too, but this would be doubling down on the bug.
更好的解决方案是将 min 和 max 放在同一个对象中,并使该对象完全null或完全非null:
A better solution is to put the min and max in the same object and make this object either fully null or fully non-null:
functionextent(nums:number[]){letresult:[number,number]|null=null;for(constnumofnums){if(!result){result=[num,num];}else{result=[Math.min(num,result[0]),Math.max(num,result[1])];}}returnresult;}
functionextent(nums:number[]){letresult:[number,number]|null=null;for(constnumofnums){if(!result){result=[num,num];}else{result=[Math.min(num,result[0]),Math.max(num,result[1])];}}returnresult;}
返回类型现在是[number, number] | null,这对客户来说更容易使用。可以使用非空断言检索最小值和最大值:
The return type is now [number, number] | null, which is easier for clients to work with. The min and max can be retrieved with either a non-null assertion:
const[min,max]=extent([0,1,2])!;constspan=max-min;// OK
const[min,max]=extent([0,1,2])!;constspan=max-min;// OK
或单张支票:
or a single check:
constrange=extent([0,1,2]);if(range){const[min,max]=range;constspan=max-min;// OK}
constrange=extent([0,1,2]);if(range){const[min,max]=range;constspan=max-min;// OK}
通过使用单个对象来跟踪范围,我们改进了设计,帮助 TypeScript 理解空值之间的关系,并修复了错误:检查if (!result)现在没有问题。
By using a single object to track the extent, we’ve improved our design, helped TypeScript understand the relationship between null values, and fixed the bug: the if (!result) check is now problem free.
空值和非空值的混合也会导致类中出现问题。例如,假设您有一个代表用户及其在论坛上的帖子的类:
A mix of null and non-null values can also lead to problems in classes. For instance, suppose you have a class that represents both a user and their posts on a forum:
classUserPosts{user:UserInfo|null;posts:Post[]|null;constructor(){this.user=null;this.posts=null;}asyncinit(userId:string){returnPromise.all([async()=>this.user=awaitfetchUser(userId),async()=>this.posts=awaitfetchPostsForUser(userId)]);}getUserName() {// ...?}}
classUserPosts{user:UserInfo|null;posts:Post[]|null;constructor(){this.user=null;this.posts=null;}asyncinit(userId:string){returnPromise.all([async()=>this.user=awaitfetchUser(userId),async()=>this.posts=awaitfetchPostsForUser(userId)]);}getUserName() {// ...?}}
在加载两个网络请求时,user和posts属性将为null. 在任何时候,他们都可能是null,一个可能是null,或者他们可能都不是null。有四种可能。这种复杂性将渗透到类中的每个方法中。这种设计几乎肯定会导致混乱、检查激增null和错误。
While the two network requests are loading, the user and posts properties will be null. At any time, they might both be null, one might be null, or they might both be non-null. There are four possibilities. This complexity will seep into every method on the class. This design is almost certain to lead to confusion, a proliferation of null checks, and bugs.
更好的设计会等到类使用的所有数据都可用:
A better design would wait until all the data used by the class is available:
classUserPosts{user:UserInfo;posts:Post[];constructor(user:UserInfo,posts:Post[]){this.user=user;this.posts=posts;}staticasyncinit(userId:string):Promise<UserPosts>{const[user,posts]=awaitPromise.all([fetchUser(userId),fetchPostsForUser(userId)]);returnnewUserPosts(user,posts);}getUserName() {returnthis.user.name;}}
classUserPosts{user:UserInfo;posts:Post[];constructor(user:UserInfo,posts:Post[]){this.user=user;this.posts=posts;}staticasyncinit(userId:string):Promise<UserPosts>{const[user,posts]=awaitPromise.all([fetchUser(userId),fetchPostsForUser(userId)]);returnnewUserPosts(user,posts);}getUserName() {returnthis.user.name;}}
现在这个UserPosts类是完全非的null,而且很容易在上面写出正确的方法。当然,如果您需要在部分加载数据时执行操作,那么您将需要处理多重状态null和非null状态。
Now the UserPosts class is fully non-null, and it’s easy to write correct methods on it. Of course, if you need to perform operations while data is partially loaded, then you’ll need to deal with the multiplicity of null and non-null states.
(不要试图替换可为空的属性与承诺。这往往会导致代码更加混乱,并迫使您的所有方法都是异步的。Promises 阐明了加载数据的代码,但往往会对使用该数据的类产生相反的影响。)
(Don’t be tempted to replace nullable properties with Promises. This tends to lead to even more confusing code and forces all your methods to be async. Promises clarify the code that loads data but tend to have the opposite effect on the class that uses that data.)
null避免一个值存在与否null与另一个值存在null与否隐含相关的设计null。
Avoid designs in which one value being null or not null is implicitly related to another value being null or not null.
通过制作更大的对象或完全不将值推null送到 API 的外围。这将使代码对于人类读者和类型检查器都更加清晰。nullnull
Push null values to the perimeter of your API by making larger objects either null or fully non-null. This will make code clearer both for human readers and for the type checker.
考虑创建一个完全非null类并在所有值都可用时构造它。
Consider creating a fully non-null class and constructing it when all values are available.
While strictNullChecks may flag many issues in your code, it’s indispensable for surfacing the behavior of functions with respect to null values.
如果您创建了一个接口,其属性是联合类型,您应该询问该类型作为更精确接口的联合是否更有意义。
If you create an interface whose properties are union types, you should ask whether the type would make more sense as a union of more precise interfaces.
假设您正在构建一个矢量绘图程序并希望为具有特定几何类型的层定义一个接口:
Suppose you’re building a vector drawing program and want to define an interface for layers with specific geometry types:
interfaceLayer{layout:FillLayout|LineLayout|PointLayout;paint:FillPaint|LinePaint|PointPaint;}
interfaceLayer{layout:FillLayout|LineLayout|PointLayout;paint:FillPaint|LinePaint|PointPaint;}
该layout字段控制绘制形状的方式和位置(圆角?直线?),而该paint字段控制样式(线是蓝色的吗?粗线?细线?虚线?)。
The layout field controls how and where the shapes are drawn (rounded corners? straight?), while the paint field controls styles (is the line blue? thick? thin? dashed?).
有一个层的属性layout是有意义吗?可能不会。允许这种可能性使得使用该库更容易出错,并使该接口难以使用。LineLayoutpaintFillPaint
Would it make sense to have a layer whose layout is LineLayout but whose paint property is FillPaint? Probably not. Allowing this possibility makes using the library more error-prone and makes this interface difficult to work with.
更好的建模方法是为每种类型的层使用单独的接口:
A better way to model this is with separate interfaces for each type of layer:
interfaceFillLayer{layout:::::::FillLayout;paint_FillPaint;}interfaceLineLayer{layout_LineLayout;paint_LinePaint;}interfacePointLayer{layout_PointLayout;paint_PointPaint;}typeLayer=FillLayer|LineLayer|PointLayer;
interfaceFillLayer{layout:FillLayout;paint:FillPaint;}interfaceLineLayer{layout:LineLayout;paint:LinePaint;}interfacePointLayer{layout:PointLayout;paint:PointPaint;}typeLayer=FillLayer|LineLayer|PointLayer;
通过以这种方式定义,您已经排除了混合和属性Layer的可能性。这是遵循Item 28的建议的一个例子,它更喜欢只代表有效状态的类型。layoutpaint
By defining Layer in this way, you’ve excluded the possibility of mixed layout and paint properties. This is an example of following Item 28’s advice to prefer types that only represent valid states.
这种模式最常见的例子是“标记联合”(或“歧视联合”)。在这种情况下,其中一个属性是字符串文字类型的联合:
The most common example of this pattern is the “tagged union” (or “discriminated union”). In this case one of the properties is a union of string literal types:
interfaceLayer{type:'fill'|'line'|'point';layout:FillLayout|LineLayout|PointLayout;paint:FillPaint|LinePaint|PointPaint;}
interfaceLayer{type:'fill'|'line'|'point';layout:FillLayout|LineLayout|PointLayout;paint:FillPaint|LinePaint|PointPaint;}
type: 'fill'和以前一样,只有一个LineLayoutand有意义吗PointPaint?当然不是。转换Layer为接口联合以排除这种可能性:
As before, would it make sense to have type: 'fill' but then a LineLayout and PointPaint? Certainly not. Convert Layer to a union of interfaces to exclude this possibility:
interfaceFillLayer{type::::::::::'fill';layout_FillLayout;paint_FillPaint;}interfaceLineLayer{type_'line';layout_LineLayout;paint_LinePaint;}interfacePointLayer{type_'paint';layout_PointLayout;paint_PointPaint;}typeLayer=FillLayer|LineLayer|PointLayer;
interfaceFillLayer{type:'fill';layout:FillLayout;paint:FillPaint;}interfaceLineLayer{type:'line';layout:LineLayout;paint:LinePaint;}interfacePointLayer{type:'paint';layout:PointLayout;paint:PointPaint;}typeLayer=FillLayer|LineLayer|PointLayer;
该type属性是“标签”,可用于确定Layer您在运行时使用的是哪种类型。LayerTypeScript 还能够根据标签缩小类型:
The type property is the “tag” and can be used to determine which type of Layer you’re working with at runtime. TypeScript is also able to narrow the type of Layer based on the tag:
functiondrawLayer(layer:Layer){if(layer.type==='fill'){const{paint}=layer;// Type is FillPaintconst{layout}=layer;// Type is FillLayout}elseif(layer.type==='line'){const{paint}=layer;// Type is LinePaintconst{layout}=layer;// Type is LineLayout}else{const{paint}=layer;// Type is PointPaintconst{layout}=layer;// Type is PointLayout}}
functiondrawLayer(layer:Layer){if(layer.type==='fill'){const{paint}=layer;// Type is FillPaintconst{layout}=layer;// Type is FillLayout}elseif(layer.type==='line'){const{paint}=layer;// Type is LinePaintconst{layout}=layer;// Type is LineLayout}else{const{paint}=layer;// Type is PointPaintconst{layout}=layer;// Type is PointLayout}}
通过正确建模此类型中属性之间的关系,您可以帮助 TypeScript 检查代码的正确性。涉及初始定义的相同代码Layer会被类型断言弄得乱七八糟。
By correctly modeling the relationship between the properties in this type, you help TypeScript check your code’s correctness. The same code involving the initial Layer definition would have been cluttered with type assertions.
因为它们与 TypeScript 的类型检查器配合得很好,所以标记联合在 TypeScript 代码中无处不在。识别这种模式并尽可能应用它。如果您可以使用标记联合在 TypeScript 中表示数据类型,那么这样做通常是个好主意。如果您将可选字段视为其类型 和 的联合undefined,那么它们也适合这种模式。考虑这种类型:
Because they work so well with TypeScript’s type checker, tagged unions are ubiquitous in TypeScript code. Recognize this pattern and apply it when you can. If you can represent a data type in TypeScript with a tagged union, it’s usually a good idea to do so. If you think of optional fields as a union of their type and undefined, then they fit this pattern as well. Consider this type:
interfacePerson{name:string;// These will either both be present or not be presentplaceOfBirth?:string;dateOfBirth?:Date;}
interfacePerson{name:string;// These will either both be present or not be presentplaceOfBirth?:string;dateOfBirth?:Date;}
带有类型信息的注释是一个强烈的信号,表明可能存在问题(第 30 项)。您尚未告知 TypeScript 的placeOfBirth和字段之间存在关系。dateOfBirth
The comment with type information is a strong sign that there might be a problem (Item 30). There is a relationship between the placeOfBirth and dateOfBirth fields that you haven’t told TypeScript about.
对此建模的更好方法是将这两个属性移动到单个对象中。这类似于将null值移动到周边(Item 31):
A better way to model this is to move both of these properties into a single object. This is akin to moving null values to the perimeter (Item 31):
interfacePerson{name:string;birth?:{place:string;date:Date;}}
interfacePerson{name:string;birth?:{place:string;date:Date;}}
Now TypeScript complains about values with a place but no date of birth:
constalanT:Person={name:'Alan Turing',birth:{// ~~~~ Property 'date' is missing in type// '{ place: string; }' but required in type// '{ place: string; date: Date; }'place:'London'}}
constalanT:Person={name:'Alan Turing',birth:{// ~~~~ Property 'date' is missing in type// '{ place: string; }' but required in type// '{ place: string; date: Date; }'place:'London'}}
此外,一个接受对象的函数Person只需要做一次检查:
Additionally, a function that takes a Person object only needs to do a single check:
functioneulogize(p:Person){console.log(p.name);const{birth}=p;if(birth){console.log(`was born on${birth.date}in${birth.place}.`);}}
functioneulogize(p:Person){console.log(p.name);const{birth}=p;if(birth){console.log(`was born on${birth.date}in${birth.place}.`);}}
如果类型的结构不在您的控制范围内(例如,它来自 API),那么您仍然可以使用现在熟悉的接口联合对这些字段之间的关系进行建模:
If the structure of the type is outside your control (e.g., it’s coming from an API), then you can still model the relationship between these fields using a now-familiar union of interfaces:
interfaceName{name:string;}interfacePersonWithBirthextendsName{placeOfBirth:string;dateOfBirth:Date;}typePerson=Name|PersonWithBirth;
interfaceName{name:string;}interfacePersonWithBirthextendsName{placeOfBirth:string;dateOfBirth:Date;}typePerson=Name|PersonWithBirth;
现在您可以获得与嵌套对象相同的一些好处:
Now you get some of the same benefits as with the nested object:
functioneulogize(p:Person){if('placeOfBirth'inp){p// Type is PersonWithBirthconst{dateOfBirth}=p// OK, type is Date}}
functioneulogize(p:Person){if('placeOfBirth'inp){p// Type is PersonWithBirthconst{dateOfBirth}=p// OK, type is Date}}
在这两种情况下,类型定义都使属性之间的关系更加清晰。
In both cases, the type definition makes the relationship between the properties more clear.
具有多个联合类型属性的接口通常是错误的,因为它们模糊了这些属性之间的关系。
Interfaces with multiple properties that are union types are often a mistake because they obscure the relationships between these properties.
接口联合更精确,可以被 TypeScript 理解。
Unions of interfaces are more precise and can be understood by TypeScript.
考虑在您的结构中添加一个“标签”,以促进 TypeScript 的控制流分析。因为它们得到了很好的支持,标记联合在 TypeScript 代码中无处不在。
Consider adding a “tag” to your structure to facilitate TypeScript’s control flow analysis. Because they are so well supported, tagged unions are ubiquitous in TypeScript code.
这类型的域string很大:"x"并且"y"在其中,但是Moby Dick的完整文本也是如此(它开始"Call me Ishmael…"并且长约 120 万个字符)。当你声明一个 type 的变量时string,你应该问一个更窄的类型是否更合适。
The domain of the string type is big: "x" and "y" are in it, but so is the complete text of Moby Dick (it starts "Call me Ishmael…" and is about 1.2 million characters long). When you declare a variable of type string, you should ask whether a narrower type would be more appropriate.
假设您正在构建一个音乐收藏并想要为专辑定义一个类型。这是一个尝试:
Suppose you’re building a music collection and want to define a type for an album. Here’s an attempt:
interfaceAlbum{artist::::string;title_string;releaseDate_string;// YYYY-MM-DDrecordingType_string;// E.g., "live" or "studio"}
interfaceAlbum{artist:string;title:string;releaseDate:string;// YYYY-MM-DDrecordingType:string;// E.g., "live" or "studio"}
类型的流行string和注释中的类型信息(参见条目 30)强烈表明这interface不是很正确。这是会出什么问题:
The prevalence of string types and the type information in comments (see Item 30) are strong indications that this interface isn’t quite right. Here’s what can go wrong:
constkindOfBlue:Album={artist:'Miles Davis',title:'Kind of Blue',releaseDate:'August 17th, 1959',// Oops!recordingType:'Studio',// Oops!};// OK
constkindOfBlue:Album={artist:'Miles Davis',title:'Kind of Blue',releaseDate:'August 17th, 1959',// Oops!recordingType:'Studio',// Oops!};// OK
该releaseDate字段格式不正确(根据评论)并且"Studio"在应该小写的地方大写。但是这些值都是字符串,所以这个对象是可赋值的Album,类型检查器不会抱怨。
The releaseDate field is incorrectly formatted (according to the comment) and "Studio" is capitalized where it should be lowercase. But these values are both strings, so this object is assignable to Album and the type checker doesn’t complain.
这些广泛的string类型也可以掩盖有效Album对象的错误。例如:
These broad string types can mask errors for valid Album objects, too. For example:
functionrecordRelease(title:string,date:string){/* ... */}recordRelease(kindOfBlue.releaseDate,kindOfBlue.title);// OK, should be error
functionrecordRelease(title:string,date:string){/* ... */}recordRelease(kindOfBlue.releaseDate,kindOfBlue.title);// OK, should be error
参数在调用中颠倒了recordRelease,但都是字符串,所以类型检查器不会抱怨。由于string类型的流行,像这样的代码有时被称为“字符串类型”。
The parameters are reversed in the call to recordRelease but both are strings, so the type checker doesn’t complain. Because of the prevalence of string types, code like this is sometimes called “stringly typed.”
您可以缩小类型以防止出现此类问题吗?虽然Moby Dick的完整文本可能是一个笨重的艺术家名称或专辑名称,但它至少是合理的。所以string适合这些领域。对于该releaseDate字段,最好只使用一个Date对象并避免格式问题。最后,对于该recordingType字段,您可以定义一个只有两个值的联合类型(您也可以使用 an enum,但我通常建议避免使用这些;请参阅条款 53):
Can you make the types narrower to prevent these sorts of issues? While the complete text of Moby Dick would be a ponderous artist name or album title, it’s at least plausible. So string is appropriate for these fields. For the releaseDate field it’s better to just use a Date object and avoid issues around formatting. Finally, for the recordingType field, you can define a union type with just two values (you could also use an enum, but I generally recommend avoiding these; see Item 53):
typeRecordingType='studio'|'live';interfaceAlbum{artist::::string;title_string;releaseDate_Date;recordingType_RecordingType;}
typeRecordingType='studio'|'live';interfaceAlbum{artist:string;title:string;releaseDate:Date;recordingType:RecordingType;}
通过这些更改,TypeScript 能够更彻底地检查错误:
With these changes TypeScript is able to do a more thorough check for errors:
constkindOfBlue:Album={artist:'Miles Davis',title:'Kind of Blue',releaseDate:newDate('1959-08-17'),recordingType:'Studio'// ~~~~~~~~~~~~ Type '"Studio"' is not assignable to type 'RecordingType'};
constkindOfBlue:Album={artist:'Miles Davis',title:'Kind of Blue',releaseDate:newDate('1959-08-17'),recordingType:'Studio'// ~~~~~~~~~~~~ Type '"Studio"' is not assignable to type 'RecordingType'};
除了更严格的检查之外,这种方法还有其他优点。首先,显式定义类型可确保其含义不会在传递时丢失。例如,如果您只想查找某种录音类型的专辑,您可以定义如下函数:
There are advantages to this approach beyond stricter checking. First, explicitly defining the type ensures that its meaning won’t get lost as it’s passed around. If you wanted to find albums of just a certain recording type, for instance, you might define a function like this:
functiongetAlbumsOfType(recordingType:string):Album[]{// ...}
functiongetAlbumsOfType(recordingType:string):Album[]{// ...}
这个函数的调用者如何知道recordingType期望的是什么?这只是一个string. 说明它"studio"隐藏"live"在 的定义中的注释Album,用户可能不会想去查看。
How does the caller of this function know what recordingType is expected to be? It’s just a string. The comment explaining that it’s "studio" or "live" is hidden in the definition of Album, where the user might not think to look.
其次,显式定义类型允许您将文档附加到它(参见条目 48):
Second, explicitly defining a type allows you attach documentation to it (see Item 48):
/** What type of environment was this recording made in? */typeRecordingType='live'|'studio';
/** What type of environment was this recording made in? */typeRecordingType='live'|'studio';
当您更改getAlbumsOfType为时RecordingType,调用者可以单击并查看文档(参见图 4-1)。
When you change getAlbumsOfType to take a RecordingType, the caller is able to click through and see the documentation (see Figure 4-1).
其他is 在函数参数中的常见误用string。假设您想编写一个函数来提取数组中单个字段的所有值。这Underscore 库称此为“pluck”:
Another common misuse of string is in function parameters. Say you want to write a function that pulls out all the values for a single field in an array. The Underscore library calls this “pluck”:
functionpluck(records,key){returnrecord.map(record=>record[key]);}
functionpluck(records,key){returnrecord.map(record=>record[key]);}
你会如何输入这个?这是一个初步尝试:
How would you type this? Here’s an initial attempt:
functionpluck(record:any[],key:string):any[]{returnrecord.map(r=>r[key]);}
functionpluck(record:any[],key:string):any[]{returnrecord.map(r=>r[key]);}
这类型检查但不是很好。这些any类型是有问题的,尤其是在返回值上(参见条目 38)。改进类型签名的第一步是引入泛型类型参数:
This type checks but isn’t great. The any types are problematic, particularly on the return value (see Item 38). The first step to improving the type signature is introducing a generic type parameter:
functionpluck<T>(record:T[],key:string):any[]{returnrecord.map(r=>r[key]);// ~~~~~~ Element implicitly has an 'any' type// because type '{}' has no index signature}
functionpluck<T>(record:T[],key:string):any[]{returnrecord.map(r=>r[key]);// ~~~~~~ Element implicitly has an 'any' type// because type '{}' has no index signature}
TypeScript 现在正在抱怨stringfor 的类型key太宽泛。这样做是正确的:如果你传入一个Albums 数组,那么只有四个有效值key(“artist”、“title”、“releaseDate”和“recordingType”),而不是大量的字符串. 这正是类型keyof Album:
TypeScript is now complaining that the string type for key is too broad. And it’s right to do so: if you pass in an array of Albums then there are only four valid values for key (“artist,” “title,” “releaseDate,” and “recordingType”), as opposed to the vast set of strings. This is precisely what the keyof Album type is:
typeK=keyofAlbum;// Type is "artist" | "title" | "releaseDate" | "recordingType"
typeK=keyofAlbum;// Type is "artist" | "title" | "releaseDate" | "recordingType"
所以解决方法是替换string为keyof T:
So the fix is to replace string with keyof T:
functionpluck<T>(record:T[],key:keyofT){returnrecord.map(r=>r[key]);}
functionpluck<T>(record:T[],key:keyofT){returnrecord.map(r=>r[key]);}
这通过了类型检查器。我们还让 TypeScript 推断返回类型。它是怎么做到的?如果将鼠标悬停pluck在编辑器中,则推断类型为:
This passes the type checker. We’ve also let TypeScript infer the return type. How does it do? If you mouse over pluck in your editor, the inferred type is:
functionpluck<T>(record:T[],key:keyofT):T[keyofT][]
functionpluck<T>(record:T[],key:keyofT):T[keyofT][]
T[keyof T]是 中任何可能值的类型T。如果您将单个字符串作为 传递key,则范围太广。例如:
T[keyof T] is the type of any possible value in T. If you’re passing in a single string as the key, this is too broad. For example:
constreleaseDates=pluck(albums,'releaseDate');// Type is (string | Date)[]
constreleaseDates=pluck(albums,'releaseDate');// Type is (string | Date)[]
类型应该是Date[],而不是(string | Date)[]。虽然keyof T比 窄得多string,但还是太宽了。keyof T为了进一步缩小范围,我们需要引入第二个通用参数,它是(可能是单个值)的子集:
The type should be Date[], not (string | Date)[]. While keyof T is much narrower than string, it’s still too broad. To narrow it further, we need to introduce a second generic parameter that is a subset of keyof T (probably a single value):
functionpluck<T,KextendskeyofT>(record:T[],key:K):T[K][]{returnrecord.map(r=>r[key]);}
functionpluck<T,KextendskeyofT>(record:T[],key:K):T[K][]{returnrecord.map(r=>r[key]);}
(有关extends这方面的更多信息,请参阅第 14 项。)
(For more on extends in this context, see Item 14.)
类型签名现在完全正确。我们可以通过pluck几种不同的方式调用来检查这一点:
The type signature is now completely correct. We can check this by calling pluck in a few different ways:
pluck(albums,'releaseDate');// Type is Date[]pluck(albums,'artist');// Type is string[]pluck(albums,'recordingType');// Type is RecordingType[]pluck(albums,'recordingDate');// ~~~~~~~~~~~~~~~ Argument of type '"recordingDate"' is not// assignable to parameter of type ...
pluck(albums,'releaseDate');// Type is Date[]pluck(albums,'artist');// Type is string[]pluck(albums,'recordingType');// Type is RecordingType[]pluck(albums,'recordingDate');// ~~~~~~~~~~~~~~~ Argument of type '"recordingDate"' is not// assignable to parameter of type ...
语言服务甚至能够在键上提供自动完成功能(如图4-2Album所示)。
The language service is even able to offer autocomplete on the keys of Album (as shown in Figure 4-2).
string与 有一些相同的问题any:如果使用不当,它会允许无效值并隐藏类型之间的关系。这会阻碍类型检查器并隐藏真正的错误。TypeScript 的能力定义的子集string是为 JavaScript 代码带来类型安全的一种强大方式。使用更精确的类型既能捕获错误又能提高代码的可读性。
string has some of the same problems as any: when used inappropriately, it permits invalid values and hides relationships between types. This thwarts the type checker and can hide real bugs. TypeScript’s ability to define subsets of string is a powerful way to bring type safety to JavaScript code. Using more precise types will both catch errors and improve the readability of your code.
避免“字符串类型”代码。优先选择更合适的类型,但并非每种类型string都有可能。
Avoid “stringly typed” code. Prefer more appropriate types where not every string is a possibility.
string如果能更准确地描述变量的域,则更喜欢字符串文字类型的联合。您将获得更严格的类型检查并改善开发体验。
Prefer a union of string literal types to string if that more accurately describes the domain of a variable. You’ll get stricter type checking and improve the development experience.
Prefer keyof T to string for function parameters that are expected to be properties of an object.
在编写类型声明时,您将不可避免地发现可以以更精确或更不精确的方式对行为进行建模的情况。类型的精确性通常是一件好事,因为它将帮助您的用户发现错误并利用 TypeScript 提供的工具。但是在提高类型声明的精度时要小心:很容易出错,不正确的类型可能比没有类型更糟糕。
In writing type declarations you’ll inevitably find situations where you can model behavior in a more precise or less precise way. Precision in types is generally a good thing because it will help your users catch bugs and take advantage of the tooling that TypeScript provides. But take care as you increase the precision of your type declarations: it’s easy to make mistakes, and incorrect types can be worse than no types at all.
认为您正在为 GeoJSON 编写类型声明,这是我们之前在条款 31中看到的一种格式。AGeoJSON 几何可以是几种类型之一,每种类型都有不同形状的坐标数组:
Suppose you are writing type declarations for GeoJSON, a format we’ve seen before in Item 31. A GeoJSON Geometry can be one of a few types, each of which have differently shaped coordinate arrays:
interfacePoint{type:::::::'Point';coordinates_number[];}interfaceLineString{type_'LineString';coordinates_number[][];}interfacePolygon{type_'Polygon';coordinates_number[][][];}typeGeometry=Point|LineString|Polygon;// Also several others
interfacePoint{type:'Point';coordinates:number[];}interfaceLineString{type:'LineString';coordinates:number[][];}interfacePolygon{type:'Polygon';coordinates:number[][][];}typeGeometry=Point|LineString|Polygon;// Also several others
这很好,但是number[]对于坐标来说有点不精确。实际上这些是纬度和经度,所以也许元组类型会更好:
This is fine, but number[] for a coordinate is a bit imprecise. Really these are latitudes and longitudes, so perhaps a tuple type would be better:
typeGeoPosition=[number,number];interfacePoint{type:'Point';coordinates:GeoPosition;}// Etc.
typeGeoPosition=[number,number];interfacePoint{type:'Point';coordinates:GeoPosition;}// Etc.
您向世界发布更精确的类型并等待奉承。不幸的是,用户抱怨您的新类型破坏了一切。即使您只使用过纬度和经度,GeoJSON 中的位置也允许具有第三个元素、海拔高度,甚至可能更多。为了使类型声明更精确,您做得太过火了,导致类型不准确!要继续使用您的类型声明,您的用户将不得不引入类型断言或使用as any.
You publish your more precise types to the world and wait for the adulation to roll in. Unfortunately, a user complains that your new types have broken everything. Even though you’ve only ever used latitude and longitude, a position in GeoJSON is allowed to have a third element, an elevation, and potentially more. In an attempt to make the type declarations more precise, you’ve gone too far and made the types inaccurate! To continue using your type declarations, your user will have to introduce type assertions or silence the type checker entirely with as any.
作为另一个例子,考虑尝试为定义在 中的类 Lisp 语言编写类型声明JSON:
As another example, consider trying to write type declarations for a Lisp-like language defined in JSON:
12 “红色的” ["+", 1, 2] // 3 ["/", 20, 2] // 10 ["case", [">", 20, 10], "red", "blue"] // "red" ["rgb", 255, 0, 127] // "#FF007F"
12 "red" ["+", 1, 2] // 3 ["/", 20, 2] // 10 ["case", [">", 20, 10], "red", "blue"] // "red" ["rgb", 255, 0, 127] // "#FF007F"
这Mapbox 库使用这样的系统来确定地图特征在许多设备上的外观。您可以尝试输入以下内容的精确度范围:
The Mapbox library uses a system like this to determine the appearance of map features across many devices. There’s a whole spectrum of precision with which you could try to type this:
允许任何事情。
Allow anything.
允许字符串、数字和数组。
Allow strings, numbers, and arrays.
允许以已知函数名称开头的字符串、数字和数组。
Allow strings, numbers, and arrays starting with known function names.
确保每个函数获得正确数量的参数。
Make sure each function gets the correct number of arguments.
确保每个函数都获得正确类型的参数。
Make sure each function gets the correct type of arguments.
前两个选项很简单:
The first two options are straightforward:
typeExpression1=any;typeExpression2=number|string|any[];
typeExpression1=any;typeExpression2=number|string|any[];
除此之外,您还应该引入一组有效表达式和无效表达式的测试集。当你使你的类型更精确时,这将有助于防止回归(参见Item 52):
Beyond this, you should introduce a test set of expressions that are valid and expressions that are not. As you make your types more precise, this will help prevent regressions (see Item 52):
consttests:Expression2[]=[10,"red",true,// ~~~ Type 'true' is not assignable to type 'Expression2'["+",10,5],["case",[">",20,10],"red","blue","green"],// Too many values["**",2,31],// Should be an error: no "**" function["rgb",255,128,64],["rgb",255,0,127,0]// Too many values];
consttests:Expression2[]=[10,"red",true,// ~~~ Type 'true' is not assignable to type 'Expression2'["+",10,5],["case",[">",20,10],"red","blue","green"],// Too many values["**",2,31],// Should be an error: no "**" function["rgb",255,128,64],["rgb",255,0,127,0]// Too many values];
要达到更高的精度,您可以使用字符串文字类型的联合作为元组的第一个元素:
To go to the next level of precision you can use a union of string literal types as the first element of a tuple:
typeFnName='+'|'-'|'*'|'/'|'>'|'<'|'case'|'rgb';typeCallExpression=[FnName,...any[]];typeExpression3=number|string|CallExpression;consttests:Expression3[]=[10,"red",true,// ~~~ Type 'true' is not assignable to type 'Expression3'["+",10,5],["case",[">",20,10],"red","blue","green"],["**",2,31],// ~~~~~~~~~~~ Type '"**"' is not assignable to type 'FnName'["rgb",255,128,64]];
typeFnName='+'|'-'|'*'|'/'|'>'|'<'|'case'|'rgb';typeCallExpression=[FnName,...any[]];typeExpression3=number|string|CallExpression;consttests:Expression3[]=[10,"red",true,// ~~~ Type 'true' is not assignable to type 'Expression3'["+",10,5],["case",[">",20,10],"red","blue","green"],["**",2,31],// ~~~~~~~~~~~ Type '"**"' is not assignable to type 'FnName'["rgb",255,128,64]];
有一个新的捕获错误并且没有回归。不错!
There’s one new caught error and no regressions. Pretty good!
什么如果您想确保每个函数都获得正确数量的参数?这变得更加棘手,因为现在需要递归类型才能深入到所有函数调用。从 TypeScript 3.6 开始,要完成这项工作,您需要至少引入一个interface. 由于interfaces 不能是联合,因此您必须改用 using 编写调用表达式interface。这有点尴尬,因为固定长度的数组最容易表示为元组类型。但你可以这样做:
What if you want to make sure that each function gets the correct number of arguments? This gets trickier since the type now needs to be recursive to reach down into all the function calls. As of TypeScript 3.6, to make this work you needed to introduce at least one interface. Since interfaces can’t be unions, you’ll have to write the call expressions using interface instead. This is a bit awkward since fixed-length arrays are most easily expressed as tuple types. But you can do it:
typeExpression4=number|string|CallExpression;typeCallExpression=MathCall|CaseCall|RGBCall;interfaceMathCall{0:'+'|'-'|'/'|'*'|'>'|'<';1:Expression4;2:Expression4;length:3;}interfaceCaseCall{0:'case';1::::Expression4;2_Expression4;3_Expression4;length_4|6|8|10|12|14|16// etc.}interfaceRGBCall{0:'rgb';1::::Expression4;2_Expression4;3_Expression4;length_4;}consttests:Expression4[]=[10,"red",true,// ~~~ Type 'true' is not assignable to type 'Expression4'["+",10,5],["case",[">",20,10],"red","blue","green"],// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// Type '["case", [">", ...], ...]' is not assignable to type 'string'["**",2,31],// ~~~~~~~~~~~~ Type '["**", number, number]' is not assignable to type 'string["rgb",255,128,64],["rgb",255,128,64,73]// ~~~~~~~~~~~~~~~~~~~~~~~~ Type '["rgb", number, number, number, number]'// is not assignable to type 'string'];
typeExpression4=number|string|CallExpression;typeCallExpression=MathCall|CaseCall|RGBCall;interfaceMathCall{0:'+'|'-'|'/'|'*'|'>'|'<';1:Expression4;2:Expression4;length:3;}interfaceCaseCall{0:'case';1:Expression4;2:Expression4;3:Expression4;length:4|6|8|10|12|14|16// etc.}interfaceRGBCall{0:'rgb';1:Expression4;2:Expression4;3:Expression4;length:4;}consttests:Expression4[]=[10,"red",true,// ~~~ Type 'true' is not assignable to type 'Expression4'["+",10,5],["case",[">",20,10],"red","blue","green"],// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~// Type '["case", [">", ...], ...]' is not assignable to type 'string'["**",2,31],// ~~~~~~~~~~~~ Type '["**", number, number]' is not assignable to type 'string["rgb",255,128,64],["rgb",255,128,64,73]// ~~~~~~~~~~~~~~~~~~~~~~~~ Type '["rgb", number, number, number, number]'// is not assignable to type 'string'];
现在所有无效的表达式都会产生错误。有趣的是,您可以使用 TypeScript 表达类似“长度均匀的数组”之类的东西interface。但是这些错误消息不是很好,而且**自从之前的输入以来,关于的错误已经变得更糟了。
Now all the invalid expressions produce errors. And it’s interesting that you can express something like “an array of even length” using a TypeScript interface. But these error messages aren’t very good, and the error about ** has gotten quite a bit worse since the previous typings.
这是对以前不太精确的类型的改进吗?事实上,你会因为一些不正确的用法而出错,这是一个胜利,但这些错误会使这种类型更难处理。语言服务与类型检查一样是 TypeScript 体验的一部分(请参阅第 6 项),因此最好查看类型声明产生的错误消息,并在它应该工作的情况下尝试自动完成。如果您的新类型声明更精确但会破坏自动完成功能,那么它们会降低 TypeScript 开发体验的乐趣。
Is this an improvement over the previous, less precise types? The fact that you get errors for some incorrect usages is a win, but the errors will make this type more difficult to work with. Language services are as much a part of the TypeScript experience as type checking (see Item 6), so it’s a good idea to look at the error messages resulting from your type declarations and try autocomplete in situations where it should work. If your new type declarations are more precise but break autocomplete, then they’ll make for a less enjoyable TypeScript development experience.
这种类型声明的复杂性也增加了错误潜入的可能性。例如,Expression4要求所有数学运算符都采用两个参数,但是Mapbox 表达式规范说明了这一点+并且*可以接受更多。此外,-可以采用单个参数,在这种情况下它会否定其输入。Expression4错误地标记了所有这些错误:
The complexity of this type declaration has also increased the odds that a bug will creep in. For example, Expression4 requires that all math operators take two parameters, but the Mapbox expression spec says that + and * can take more. Also, - can take a single parameter, in which case it negates its input. Expression4 incorrectly flags errors in all of these:
constokExpressions:Expression4[]=[['-',12],// ~~~~~~~~~ Type '["-", number]' is not assignable to type 'string'['+',1,2,3],// ~~~~~~~~~~~~~~ Type '["+", number, ...]' is not assignable to type 'string'['*',2,3,4],// ~~~~~~~~~~~~~~ Type '["*", number, ...]' is not assignable to type 'string'];
constokExpressions:Expression4[]=[['-',12],// ~~~~~~~~~ Type '["-", number]' is not assignable to type 'string'['+',1,2,3],// ~~~~~~~~~~~~~~ Type '["+", number, ...]' is not assignable to type 'string'['*',2,3,4],// ~~~~~~~~~~~~~~ Type '["*", number, ...]' is not assignable to type 'string'];
再一次,在试图更精确的过程中,我们已经过头了,变得不准确了。这些不准确之处可以得到纠正,但您需要扩展您的测试集以说服自己您没有遗漏任何其他内容。复杂的代码通常需要更多的测试,类型也是如此。
Once again, in trying to be more precise we’ve overshot and become inaccurate. These inaccuracies can be corrected, but you’ll want to expand your test set to convince yourself that you haven’t missed anything else. Complex code generally requires more tests, and the same is true of types.
在优化类型时,想想“恐怖谷”的比喻会很有帮助。精炼非常不精确的类型any通常是有帮助的。但是随着您的类型变得更加精确,对它们也将准确的期望也会增加。您将开始更多地依赖类型,因此不准确会产生更大的问题。
As you refine types, it can be helpful to think of the “uncanny valley” metaphor. Refining very imprecise types like any is usually helpful. But as your types get more precise, the expectation that they’ll also be accurate increases. You’ll start to rely on the types more, and so inaccuracies will produce bigger problems.
避免类型安全的恐怖谷:不正确的类型通常比没有类型更糟糕。
Avoid the uncanny valley of type safety: incorrect types are often worse than no types.
If you cannot model a type accurately, do not model it inaccurately! Acknowledge the gaps using any or unknown.
Pay attention to error messages and autocomplete as you make typings increasingly precise. It’s not just about correctness: developer experience matters, too.
这本章的其他项目讨论了设计良好的类型的许多好处,并说明了如果你不这样做会出现什么问题。设计良好的类型使 TypeScript 使用起来很愉快,而设计不佳的类型会使它变得痛苦。但这确实给类型设计带来了不小的压力。如果您不必自己做这不是很好吗?
The other items in this chapter have discussed the many benefits of designing your types well and shown what can go wrong if you don’t. A well-designed type makes TypeScript a pleasure to use, while a poorly designed one can make it miserable. But this does put quite a bit of pressure on type design. Wouldn’t it be nice if you didn’t have to do this yourself?
至少您的某些类型可能来自您的程序外部:文件格式、API 或规范。在这些情况下,您可以通过生成类型来避免编写类型。如果这样做,关键是从规范生成类型,而不是从示例数据生成类型。当您从规范生成类型时,TypeScript 将帮助确保您没有遗漏任何情况。当您从数据生成类型时,您只是在考虑您所看到的示例。您可能会遗漏可能破坏程序的重要边缘情况。
At least some of your types are likely to come from outside your program: file formats, APIs, or specs. In these cases you may be able to avoid writing types by generating them instead. If you do this, the key is to generate types from specifications, rather than from example data. When you generate types from a spec, TypeScript will help ensure that you haven’t missed any cases. When you generate types from data, you’re only considering the examples you’ve seen. You might be missing important edge cases that could break your program.
在项目 31中,我们编写了一个函数来计算 a 的边界框GeoJSON 功能。这是它的样子:
In Item 31 we wrote a function to calculate the bounding box of a GeoJSON Feature. Here’s what it looked like:
functioncalculateBoundingBox(f:GeoJSONFeature):BoundingBox|null{letbox:BoundingBox|null=null;consthelper=(coords:any[])=>{// ...};const{geometry}=f;if(geometry){helper(geometry.coordinates);}returnbox;}
functioncalculateBoundingBox(f:GeoJSONFeature):BoundingBox|null{letbox:BoundingBox|null=null;consthelper=(coords:any[])=>{// ...};const{geometry}=f;if(geometry){helper(geometry.coordinates);}returnbox;}
该GeoJSONFeature类型从未明确定义。您可以使用条款 31中的一些示例来编写它。但更好的方法是使用正式的 GeoJSON 规范。1对我们来说幸运的是,在 DefinitelyTyped 上已经有它的 TypeScript 类型声明。您可以按照通常的方式添加这些:
The GeoJSONFeature type was never explicitly defined. You could write it using some of the examples from Item 31. But a better approach is to use the formal GeoJSON spec.1 Fortunately for us, there are already TypeScript type declarations for it on DefinitelyTyped. You can add these in the usual way:
$ npm install --save-dev @types/geojson + @types/geojson@7946.0.7
$ npm install --save-dev @types/geojson + @types/geojson@7946.0.7
当您插入 GeoJSON 声明时,TypeScript 会立即标记错误:
When you plug in the GeoJSON declarations, TypeScript immediately flags an error:
import{Feature}from'geojson';functioncalculateBoundingBox(f:Feature):BoundingBox|null{letbox:BoundingBox|null=null;consthelper=(coords:any[])=>{// ...};const{geometry}=f;if(geometry){helper(geometry.coordinates);// ~~~~~~~~~~~// Property 'coordinates' does not exist on type 'Geometry'// Property 'coordinates' does not exist on type// 'GeometryCollection'}returnbox;}
import{Feature}from'geojson';functioncalculateBoundingBox(f:Feature):BoundingBox|null{letbox:BoundingBox|null=null;consthelper=(coords:any[])=>{// ...};const{geometry}=f;if(geometry){helper(geometry.coordinates);// ~~~~~~~~~~~// Property 'coordinates' does not exist on type 'Geometry'// Property 'coordinates' does not exist on type// 'GeometryCollection'}returnbox;}
问题是您的代码假定几何图形将具有属性coordinates。许多几何图形都是如此,包括点、线和多边形。但是 GeoJSON 几何图形也可以是GeometryCollection其他几何图形的异构集合。与其他几何类型不同,它没有属性coordinates。
The problem is that your code assumes a geometry will have a coordinates property. This is true for many geometries, including points, lines, and polygons. But a GeoJSON geometry can also be a GeometryCollection, a heterogeneous collection of other geometries. Unlike the other geometry types, it does not have a coordinates property.
如果您调用calculateBoundingBox几何为 a 的 Feature GeometryCollection,它将抛出无法读取 的属性0的错误undefined。这是一个真正的错误!我们使用规范中的类型定义捕获了它。
If you call calculateBoundingBox on a Feature whose geometry is a GeometryCollection, it will throw an error about not being able to read property 0 of undefined. This is a real bug! And we caught it using type definitions from a spec.
修复它的一种选择是明确禁止GeometryCollections,如下所示:
One option for fixing it is to explicitly disallow GeometryCollections, as shown here:
const{geometry}=f;if(geometry){if(geometry.type==='GeometryCollection'){thrownewError('GeometryCollections are not supported.');}helper(geometry.coordinates);// OK}
const{geometry}=f;if(geometry){if(geometry.type==='GeometryCollection'){thrownewError('GeometryCollections are not supported.');}helper(geometry.coordinates);// OK}
TypeScript 能够geometry根据检查细化 of 的类型,因此geometry.coordinates允许引用 to。如果不出意外,这会为用户带来更清晰的错误消息。
TypeScript is able to refine the type of geometry based on the check, so the reference to geometry.coordinates is allowed. If nothing else, this results in a clearer error message for the user.
但更好的解决方案是支持所有类型的几何体!你可以通过拉出另一个辅助函数来做到这一点:
But the better solution is to support all the types of geometry! You can do this by pulling out another helper function:
constgeometryHelper=(g:Geometry)=>{if(geometry.type==='GeometryCollection'){geometry.geometries.forEach(geometryHelper);}else{helper(geometry.coordinates);// OK}}const{geometry}=f;if(geometry){geometryHelper(geometry);}
constgeometryHelper=(g:Geometry)=>{if(geometry.type==='GeometryCollection'){geometry.geometries.forEach(geometryHelper);}else{helper(geometry.coordinates);// OK}}const{geometry}=f;if(geometry){geometryHelper(geometry);}
如果您自己为 GeoJSON 编写类型声明,您将基于您对该格式的理解和经验。这可能没有包含GeometryCollections 并且会导致对代码正确性的错误安全感。使用基于规范的类型让您确信您的代码将适用于所有值,而不仅仅是您所看到的值。
Had you written type declarations for GeoJSON yourself, you would have based them off of your understanding and experience with the format. This might not have included GeometryCollections and would have led to a false sense of security about your code’s correctness. Using types based on a spec gives you confidence that your code will work with all values, not just the ones you’ve seen.
类似的考虑适用于 API 调用:如果您可以从 API 规范生成类型,那么这样做通常是个好主意。这特别适用于自身类型化的 API,例如 GraphQL。
Similar considerations apply to API calls: if you can generate types from the specification of an API, then it is usually a good idea to do so. This works particularly well with APIs that are typed themselves, such as GraphQL.
GraphQL API 带有一个模式,该模式使用有点类似于 TypeScript 的类型系统指定所有可能的查询和接口。您编写查询以请求这些接口中的特定字段。例如,要使用 GitHub GraphQL API 获取有关存储库的信息,您可以编写:
A GraphQL API comes with a schema that specifies all the possible queries and interfaces using a type system somewhat similar to TypeScript. You write queries that request specific fields in these interfaces. For example, to get information about a repository using the GitHub GraphQL API you might write:
询问 {
存储库(所有者:“Microsoft”,名称:“TypeScript”){
创建于
描述
}
}query {
repository(owner: "Microsoft", name: "TypeScript") {
createdAt
description
}
}
结果是:
The result is:
{"data":{"repository":{"createdAt":"2014-06-17T15:28:39Z","description":"TypeScript is a superset of JavaScript that compiles to JavaScript."}}}
{"data":{"repository":{"createdAt":"2014-06-17T15:28:39Z","description":"TypeScript is a superset of JavaScript that compiles to JavaScript."}}}
这种方法的优点在于您可以为您的特定查询生成 TypeScript 类型。与 GeoJSON 示例一样,这有助于确保您准确建模类型之间的关系及其可空性。
The beauty of this approach is that you can generate TypeScript types for your specific query. As with the GeoJSON example, this helps ensure that you model the relationships between types and their nullability accurately.
这是获取 GitHub 存储库的开源许可证的查询:
Here’s a query to get the open source license for a GitHub repository:
查询 getLicense($owner:String!, $name:String!){
存储库(所有者:$所有者,名称:$名称){
描述
许可证信息{
spdxId
姓名
}
}
}query getLicense($owner:String!, $name:String!){
repository(owner:$owner, name:$name) {
description
licenseInfo {
spdxId
name
}
}
}
$owner并且$name是本身类型化的 GraphQL 变量。类型语法与 TypeScript 非常相似,来回切换可能会造成混淆。String是一个 GraphQL 类型——它会string在 TypeScript 中(见条款 10)。虽然 TypeScript 类型不可为空,但 GraphQL 中的类型可以。类型后!的表示保证不为空。
$owner and $name are GraphQL variables which are themselves typed. The type syntax is similar enough to TypeScript that it can be confusing to go back and forth. String is a GraphQL type—it would be string in TypeScript (see Item 10). And while TypeScript types are not nullable, types in GraphQL are. The ! after the type indicates that it is guaranteed to not be null.
有许多工具可以帮助您从 GraphQL 查询转换为 TypeScript 类型。一个是阿波罗。以下是您如何使用它:
There are many tools to help you go from a GraphQL query to TypeScript types. One is Apollo. Here’s how you use it:
$阿波罗客户端:codegen \
--端点 https://api.github.com/graphql \
--包括许可证.graphql \
--目标TypeScript
加载阿波罗计划
使用“TypeScript ”目标生成查询文件 - 写了 2 个文件$ apollo client:codegen \
--endpoint https://api.github.com/graphql \
--includes license.graphql \
--target typescript
Loading Apollo Project
Generating query files with 'typescript' target - wrote 2 files
您需要一个 GraphQL 架构来为查询生成类型。阿波罗从端点得到这个api.github.com/graphql。输出如下所示:
You need a GraphQL schema to generate types for a query. Apollo gets this from the api.github.com/graphql endpoint. The output looks like this:
exportinterfacegetLicense_repository_licenseInfo{__typename:"License";/** Short identifier specified by <https://spdx.org/licenses> */spdxId:string|null;/** The license full name specified by <https://spdx.org/licenses> */name:string;}exportinterfacegetLicense_repository{__typename:"Repository";/** The description of the repository. */description:string|null;/** The license associated with the repository */licenseInfo:getLicense_repository_licenseInfo|null;}exportinterfacegetLicense{/** Lookup a given repository by the owner and repository name. */repository:getLicense_repository|null;}exportinterfacegetLicenseVariables{owner:string;name:string;}
exportinterfacegetLicense_repository_licenseInfo{__typename:"License";/** Short identifier specified by <https://spdx.org/licenses> */spdxId:string|null;/** The license full name specified by <https://spdx.org/licenses> */name:string;}exportinterfacegetLicense_repository{__typename:"Repository";/** The description of the repository. */description:string|null;/** The license associated with the repository */licenseInfo:getLicense_repository_licenseInfo|null;}exportinterfacegetLicense{/** Lookup a given repository by the owner and repository name. */repository:getLicense_repository|null;}exportinterfacegetLicenseVariables{owner:string;name:string;}
这里要注意的重要一点是:
The important bits to note here are that:
getLicenseVariables为查询参数 ( ) 和响应 ( )生成接口getLicense。
Interfaces are generated for both the query parameters (getLicenseVariables) and the response (getLicense).
可空性信息从模式传输到响应接口。、、和字段可以为空,而许可证和查询repository变量则不能。descriptionlicenseInfospdxIdname
Nullability information is transferred from the schema to the response interfaces. The repository, description, licenseInfo, and spdxId fields are nullable, whereas the license name and the query variables are not.
文档以 JSDoc 的形式传输,以便它出现在您的编辑器中(条目 48)。这些注释来自 GraphQL 模式本身。
Documentation is transferred as JSDoc so that it appears in your editor (Item 48). These comments come from the GraphQL schema itself.
此类型信息有助于确保您正确使用 API。如果您的查询发生变化,类型也会发生变化。如果架构发生变化,那么您的类型也会发生变化。您的类型和现实不存在差异是没有风险的,因为它们都来自同一个事实来源:GraphQL 模式。
This type information helps ensure that you use the API correctly. If your queries change, the types will change. If the schema changes, then so will your types. There is no risk that your types and reality diverge since they are both coming from a single source of truth: the GraphQL schema.
如果没有可用的规范或官方架构怎么办?然后你必须从数据中生成类型。像这样的工具quicktype可以帮助解决这个问题。但请注意,您的类型可能与现实不符:您可能错过了一些边缘情况。
What if there’s no spec or official schema available? Then you’ll have to generate types from data. Tools like quicktype can help with this. But be aware that your types may not match reality: there may be edge cases that you’ve missed.
即使您没有意识到这一点,您也已经从代码生成中受益。浏览器 DOM API 的 TypeScript 类型声明是从官方接口生成的(参见条目 55)。这可确保它们正确地建模一个复杂的系统,并帮助 TypeScript 捕获您自己代码中的错误和误解。
Even if you’re not aware of it, you are already benefiting from code generation. TypeScript’s type declarations for the browser DOM API are generated from the official interfaces (see Item 55). This ensures that they correctly model a complicated system and helps TypeScript catch errors and misunderstandings in your own code.
计算机科学中只有两个难题:缓存失效和命名事物。
菲尔·卡尔顿
There are only two hard problems in Computer Science: cache invalidation and naming things.
Phil Karlton
这本书对类型的形状和它们领域中的值集有很多论述,但对类型的命名却少之又少。但这也是类型设计的重要组成部分。精心选择的类型、属性和变量名称可以阐明意图并提高代码和类型的抽象级别。选择不当的类型会使您的代码模糊不清并导致不正确的心智模型。
This book has had much to say about the shape of types and the sets of values in their domains, but much less about what you name your types. But this is an important part of type design, too. Well-chosen type, property, and variable names can clarify intent and raise the level of abstraction of your code and types. Poorly chosen types can obscure your code and lead to incorrect mental models.
假设您正在构建一个动物数据库。您创建一个接口来表示一个:
Suppose you’re building out a database of animals. You create an interface to represent one:
interfaceAnimal{name:string;endangered:boolean;habitat:string;}constleopard:Animal={name:'Snow Leopard',endangered:false,habitat:'tundra',};
interfaceAnimal{name:string;endangered:boolean;habitat:string;}constleopard:Animal={name:'Snow Leopard',endangered:false,habitat:'tundra',};
这里有几个问题:
There are a few issues here:
name是一个非常笼统的术语。你期待什么样的名字?学名?一个普通的名字?
name is a very general term. What sort of name are you expecting? A scientific name? A common name?
布尔endangered字段也是模棱两可的。如果动物灭绝了怎么办?这里的意图是“濒危还是更糟?” 或者它的字面意思是濒危?
The boolean endangered field is also ambiguous. What if an animal is extinct? Is the intent here “endangered or worse?” Or does it literally mean endangered?
该habitat字段非常模糊,不仅是因为string类型过于宽泛(条目 33),还因为不清楚“栖息地”的含义。
The habitat field is very ambiguous, not just because of the overly broad string type (Item 33) but also because it’s unclear what’s meant by “habitat.”
变量名称为leopard,但属性值为name“Snow Leopard”。这种区别有意义吗?
The variable name is leopard, but the value of the name property is “Snow Leopard.” Is this distinction meaningful?
这是一个歧义较少的类型声明和值:
Here’s a type declaration and value with less ambiguity:
interfaceAnimal{commonName:::::::string;genus_string;species_string;status_ConservationStatus;climates_KoppenClimate[];}typeConservationStatus='EX'|'EW'|'CR'|'EN'|'VU'|'NT'|'LC';typeKoppenClimate=|'Af'|'Am'|'As'|'Aw'|'BSh'|'BSk'|'BWh'|'BWk'|'Cfa'|'Cfb'|'Cfc'|'Csa'|'Csb'|'Csc'|'Cwa'|'Cwb'|'Cwc'|'Dfa'|'Dfb'|'Dfc'|'Dfd'|'Dsa'|'Dsb'|'Dsc'|'Dwa'|'Dwb'|'Dwc'|'Dwd'|'EF'|'ET';constsnowLeopard_Animal={commonName:'Snow Leopard',genus:'Panthera',species:'Uncia',status:'VU',// vulnerableclimates:['ET','EF','Dfd'],// alpine or subalpine};
interfaceAnimal{commonName:string;genus:string;species:string;status:ConservationStatus;climates:KoppenClimate[];}typeConservationStatus='EX'|'EW'|'CR'|'EN'|'VU'|'NT'|'LC';typeKoppenClimate=|'Af'|'Am'|'As'|'Aw'|'BSh'|'BSk'|'BWh'|'BWk'|'Cfa'|'Cfb'|'Cfc'|'Csa'|'Csb'|'Csc'|'Cwa'|'Cwb'|'Cwc'|'Dfa'|'Dfb'|'Dfc'|'Dfd'|'Dsa'|'Dsb'|'Dsc'|'Dwa'|'Dwb'|'Dwc'|'Dwd'|'EF'|'ET';constsnowLeopard:Animal={commonName:'Snow Leopard',genus:'Panthera',species:'Uncia',status:'VU',// vulnerableclimates:['ET','EF','Dfd'],// alpine or subalpine};
这使得一些改进:
This makes a number of improvements:
name已替换为更具体的术语:commonName、genus和species。
name has been replaced with more specific terms: commonName, genus, and species.
endangered已成为conservationStatus并使用 IUCN 的标准分类系统。
endangered has become conservationStatus and uses a standard classification system from the IUCN.
habitat已成为climates并使用另一种标准分类法,即柯本气候分类法。
habitat has become climates and uses another standard taxonomy, the Köppen climate classification.
如果您需要有关此类型第一个版本中的字段的更多信息,您必须去找编写它们的人并询问。他们很可能已经离开公司或不记得了。更糟糕的是,您可能会跑去git blame找出是谁写了这些糟糕的类型,结果却发现是您!
If you needed more information about the fields in the first version of this type, you’d have to go find the person who wrote them and ask. In all likelihood, they’ve left the company or don’t remember. Worse yet, you might run git blame to find out who wrote these lousy types, only to find that it was you!
第二个版本的情况有了很大改善。如果您想了解有关柯本气候分类系统的更多信息或了解保护状态的确切含义,那么网上有无数资源可以为您提供帮助。
The situation is much improved with the second version. If you want to learn more about the Köppen climate classification system or track down what the precise meaning of a conservation status is, then there are myriad resources online to help you.
每个领域都有专门的词汇来描述其主题。与其发明自己的术语,不如尝试重用问题领域中的术语。这些词汇通常经过数年、数十年或数百年的磨练,并为该领域的人们所熟知。使用这些术语将帮助您与用户交流并提高类型的清晰度。
Every domain has specialized vocabulary to describe its subject. Rather than inventing your own terms, try to reuse terms from the domain of your problem. These vocabularies have often been honed over years, decades, or centuries and are well understood by people in the field. Using these terms will help you communicate with users and increase the clarity of your types.
注意准确使用领域词汇:选择领域语言来表示不同的意思比发明自己的语言更令人困惑。
Take care to use domain vocabulary accurately: co-opting the language of a domain to mean something different is even more confusing than inventing your own.
在命名类型、属性和变量时,请记住以下一些其他规则:
Here are a few other rules to keep in mind as you name types, properties, and variables:
让区分有意义。在写作和演讲中,一遍又一遍地使用同一个词可能会很乏味。我们引入同义词来打破单调。这使散文阅读起来更有趣,但对代码却有相反的效果。如果您使用两个不同的术语,请确保您进行了有意义的区分。如果不是,则应使用相同的术语。
Make distinctions meaningful. In writing and speech it can be tedious to use the same word over and over. We introduce synonyms to break the monotony. This makes prose more enjoyable to read, but it has the opposite effect on code. If you use two different terms, make sure you’re drawing a meaningful distinction. If not, you should use the same term.
避免使用模糊、无意义的名称,例如“数据”、“信息”、“事物”、“项目”、“对象”或一直流行的“实体”。如果 Entity 在您的领域中具有特定含义,那很好。但是,如果您使用它是因为您不想想一个更有意义的名称,那么您最终会遇到麻烦。
Avoid vague, meaningless names like “data,” “info,” “thing,” “item,” “object,” or the ever-popular “entity.” If Entity has a specific meaning in your domain, fine. But if you’re using it because you don’t want to think of a more meaningful name, then you’ll eventually run into trouble.
根据事物的本质来命名事物,而不是根据它们包含的内容或计算方式来命名。Directory比 更有意义INodeList。它允许您将目录视为一个概念,而不是其实现。好的名字可以增加你的抽象层次并降低意外碰撞的风险。
Name things for what they are, not for what they contain or how they are computed. Directory is more meaningful than INodeList. It allows you to think about a directory as a concept, rather than in terms of its implementation. Good names can increase your level of abstraction and decrease your risk of inadvertent collisions.
讨论的项目 4结构(“鸭子”)类型以及它有时如何导致令人惊讶的结果:
Item 4 discussed structural (“duck”) typing and how it can sometimes lead to surprising results:
interfaceVector2D{x:number;y:number;}functioncalculateNorm(p:Vector2D){returnMath.sqrt(p.x*p.x+p.y*p.y);}calculateNorm({x::::::3,y_4});// OK, result is 5constvec3D={x_3,y_4,z_1};calculateNorm(vec3D);// OK! result is also 5
interfaceVector2D{x:number;y:number;}functioncalculateNorm(p:Vector2D){returnMath.sqrt(p.x*p.x+p.y*p.y);}calculateNorm({x:3,y:4});// OK, result is 5constvec3D={x:3,y:4,z:1};calculateNorm(vec3D);// OK! result is also 5
如果您想calculateNorm拒绝 3D 矢量怎么办?这违背了 TypeScript 的结构类型模型,但在数学上肯定更正确。
What if you’d like calculateNorm to reject 3D vectors? This goes against the structural typing model of TypeScript but is certainly more mathematically correct.
实现此目的的一种方法是使用名义类型。对于标称类型,一个值是 aVector2D因为你说它是,而不是因为它有正确的形状。为了在 TypeScript 中近似这个,你可以引入一个“品牌”(想想奶牛,而不是可口可乐):
One way to achieve this is with nominal typing. With nominal typing, a value is a Vector2D because you say it is, not because it has the right shape. To approximate this in TypeScript, you can introduce a “brand” (think cows, not Coca-Cola):
interfaceVector2D{_brand:'2d';x::::::number;y_number;}functionvec2D(x_number,y_number):Vector2D{return{x,y,_brand:'2d'};}functioncalculateNorm(p_Vector2D){returnMath.sqrt(p.x*p.x+p.y*p.y);// Same as before}calculateNorm(vec2D(3,4));// OK, returns 5constvec3D={x:3,y:4,z:1};calculateNorm(vec3D);// ~~~~~ Property '_brand' is missing in type...
interfaceVector2D{_brand:'2d';x:number;y:number;}functionvec2D(x:number,y:number):Vector2D{return{x,y,_brand:'2d'};}functioncalculateNorm(p:Vector2D){returnMath.sqrt(p.x*p.x+p.y*p.y);// Same as before}calculateNorm(vec2D(3,4));// OK, returns 5constvec3D={x:3,y:4,z:1};calculateNorm(vec3D);// ~~~~~ Property '_brand' is missing in type...
该品牌确保载体来自正确的地方。当然,没有什么能阻止您增加_brand: '2d'价值vec3D。但这是从意外转变为恶意。这种品牌通常足以捕捉到无意中滥用功能。
The brand ensures that the vector came from the right place. Granted there’s nothing stopping you from adding _brand: '2d' to the vec3D value. But this is moving from the accidental into the malicious. This sort of brand is typically enough to catch inadvertent misuses of functions.
有趣的是,仅在类型系统中运行时,您可以获得与显式品牌相同的许多好处。这消除了运行时开销,还允许您标记内置类型,例如string或number您无法附加其他属性的地方。
Interestingly, you can get many of the same benefits as explicit brands while operating only in the type system. This removes runtime overhead and also lets you brand built-in types like string or number where you can’t attach additional properties.
例如,如果您有一个在文件系统上运行的函数并且需要绝对(而不是相对)路径怎么办?这在运行时很容易检查(路径是否以“/”开头?)但在类型系统中就不那么容易了。
For instance, what if you have a function that operates on the filesystem and requires an absolute (as opposed to a relative) path? This is easy to check at runtime (does the path start with “/”?) but not so easy in the type system.
这是品牌的一种方法:
Here’s an approach with brands:
typeAbsolutePath=string&{_brand:'abs'};functionlistAbsolutePath(path:AbsolutePath){// ...}functionisAbsolutePath(path:string):pathisAbsolutePath{returnpath.startsWith('/');}
typeAbsolutePath=string&{_brand:'abs'};functionlistAbsolutePath(path:AbsolutePath){// ...}functionisAbsolutePath(path:string):pathisAbsolutePath{returnpath.startsWith('/');}
您不能构造一个具有string属性的对象_brand。这纯粹是一个类型系统的游戏。
You can’t construct an object that is a string and has a _brand property. This is purely a game with the type system.
如果您的string路径可以是绝对路径或相对路径,则可以使用类型保护进行检查,这将细化其类型:
If you have a string path that could be either absolute or relative, you can check using the type guard, which will refine its type:
functionf(path:string){if(isAbsolutePath(path)){listAbsolutePath(path);}listAbsolutePath(path);// ~~~~ Argument of type 'string' is not assignable// to parameter of type 'AbsolutePath'}
functionf(path:string){if(isAbsolutePath(path)){listAbsolutePath(path);}listAbsolutePath(path);// ~~~~ Argument of type 'string' is not assignable// to parameter of type 'AbsolutePath'}
这种方法可能有助于记录哪些函数需要绝对或相对路径,以及每个变量持有哪种类型的路径。不过,这并不是铁定的保证:path as AbsolutePath任何string. 但是,如果您避免这些类型的断言,那么获得 an 的唯一方法AbsolutePath就是给出一个或进行检查,这正是您想要的。
This sort of approach could be helpful in documenting which functions expect absolute or relative paths and which type of path each variable holds. It is not an ironclad guarantee, though: path as AbsolutePath will succeed for any string. But if you avoid these sorts of assertions, then the only way to get an AbsolutePath is to be given one or to check, which is exactly what you want.
这种方法可用于对无法在类型系统中表达的许多属性进行建模。例如,使用二进制搜索查找列表中的元素:
This approach can be used to model many properties that cannot be expressed within the type system. For example, using binary search to find an element in a list:
functionbinarySearch<T>(xs:T[],x:T):boolean{letlow=0,high=xs.length-1;while(high>=low){constmid=low+Math.floor((high-low)/2);constv=xs[mid];if(v===x)returntrue;[low,high]=x>v?[mid+1,high]:[low,mid-1];}returnfalse;}
functionbinarySearch<T>(xs:T[],x:T):boolean{letlow=0,high=xs.length-1;while(high>=low){constmid=low+Math.floor((high-low)/2);constv=xs[mid];if(v===x)returntrue;[low,high]=x>v?[mid+1,high]:[low,mid-1];}returnfalse;}
如果列表已排序,则此方法有效,但如果未排序,则会导致漏报。您不能在 TypeScript 的类型系统中表示排序列表。但是你可以创建一个品牌:
This works if the list is sorted, but will result in false negatives if it is not. You can’t represent a sorted list in TypeScript’s type system. But you can create a brand:
typeSortedList<T>=T[]&{_brand:'sorted'};functionisSorted<T>(xs:T[]):xsisSortedList<T>{for(leti=1;i<xs.length;i++){if(xs[i]>xs[i-1]){returnfalse;}}returntrue;}functionbinarySearch<T>(xs:SortedList<T>,x:T):boolean{// ...}
typeSortedList<T>=T[]&{_brand:'sorted'};functionisSorted<T>(xs:T[]):xsisSortedList<T>{for(leti=1;i<xs.length;i++){if(xs[i]>xs[i-1]){returnfalse;}}returntrue;}functionbinarySearch<T>(xs:SortedList<T>,x:T):boolean{// ...}
为了调用这个版本的binarySearch,您需要得到一个SortedList(即证明列表已排序)或证明它是您自己使用 排序的isSorted。线性扫描不是很好,但至少你是安全的!
In order to call this version of binarySearch, you either need to be given a SortedList (i.e., have a proof that the list is sorted) or prove that it’s sorted yourself using isSorted. The linear scan isn’t great, but at least you’ll be safe!
一般来说,这是对类型检查器有帮助的观点。例如,为了在一个对象上调用一个方法,您要么需要给定一个非对象,要么用条件null证明它不是您自己。null
This is a helpful perspective to have on the type checker in general. In order to call a method on an object, for instance, you either need to be given a non-null object or prove that it’s non-null yourself with a conditional.
您还可以标记number类型——例如,附加单元:
You can also brand number types—for example, to attach units:
typeMeters=number&{_brand:'meters'};typeSeconds=number&{_brand:'seconds'};constmeters=(m:number)=>masMeters;constseconds=(s:number)=>sasSeconds;constoneKm=meters(1000);// Type is MetersconstoneMin=seconds(60);// Type is Seconds
typeMeters=number&{_brand:'meters'};typeSeconds=number&{_brand:'seconds'};constmeters=(m:number)=>masMeters;constseconds=(s:number)=>sasSeconds;constoneKm=meters(1000);// Type is MetersconstoneMin=seconds(60);// Type is Seconds
这在实践中可能很尴尬,因为算术运算会使数字忘记它们的品牌:
This can be awkward in practice since arithmetic operations make the numbers forget their brands:
consttenKm=oneKm*10;// Type is numberconstv=oneKm/oneMin;// Type is number
consttenKm=oneKm*10;// Type is numberconstv=oneKm/oneMin;// Type is number
但是,如果您的代码涉及大量具有混合单位的数字,这可能仍然是一种记录预期数字参数类型的有吸引力的方法。
If your code involves lots of numbers with mixed units, however, this may still be an attractive approach to documenting the expected types of numeric parameters.
TypeScript 使用结构化(“鸭子”)类型,这有时会导致令人惊讶的结果。如果您需要名义打字,请考虑将“品牌”附加到您的值以区分它们。
TypeScript uses structural (“duck”) typing, which can sometimes lead to surprising results. If you need nominal typing, consider attaching “brands” to your values to distinguish them.
在某些情况下,您可以完全在类型系统中而不是在运行时附加品牌。您可以使用此技术对 TypeScript 类型系统之外的属性进行建模。
In some cases you may be able to attach brands entirely in the type system, rather than at runtime. You can use this technique to model properties outside of TypeScript’s type system.
1个GeoJSON 也称为 RFC 7946。可读性很强的规范位于http://geojson.org。
1 GeoJSON is also known as RFC 7946. The very readable spec is at http://geojson.org.
类型系统传统上是二元事务:一种语言要么具有完全静态的类型系统,要么具有完全动态的类型系统。TypeScript 模糊了界限,因为它的类型系统是可选的和渐进的。您可以向程序的某些部分添加类型,但不能向其他部分添加类型。
Type systems were traditionally binary affairs: either a language had a fully static type system or a fully dynamic one. TypeScript blurs the line, because its type system is optional and gradual. You can add types to parts of your program but not others.
这对于将现有的 JavaScript 代码库一点一点地迁移到 TypeScript 是必不可少的(第 8 章)。关键是类型any,它有效地禁用了部分代码的类型检查。它既强大又容易被滥用。学会明智地使用any对于编写有效的 TypeScript 至关重要。本章将向您介绍如何any在保留其优点的同时限制其缺点。
This is essential for migrating existing JavaScript codebases to TypeScript bit by bit (Chapter 8). Key to this is the any type, which effectively disables type checking for parts of your code. It is both powerful and prone to abuse. Learning to use any wisely is essential for writing effective TypeScript. This chapter walks you through how to limit the downsides of any while still retaining its benefits.
functionprocessBar(b:Bar){/* ... */}functionf() {constx=expressionReturningFoo();processBar(x);// ~ Argument of type 'Foo' is not assignable to// parameter of type 'Bar'}
functionprocessBar(b:Bar){/* ... */}functionf() {constx=expressionReturningFoo();processBar(x);// ~ Argument of type 'Foo' is not assignable to// parameter of type 'Bar'}
如果您以某种方式从 context 中知道xassignable toBar除了 之外Foo,您可以强制 TypeScript 通过两种方式接受此代码:
If you somehow know from context that x is assignable to Bar in addition to Foo, you can force TypeScript to accept this code in two ways:
functionf1() {constx:any=expressionReturningFoo();// Don't do thisprocessBar(x);}functionf2() {constx=expressionReturningFoo();processBar(xasany);// Prefer this}
functionf1() {constx:any=expressionReturningFoo();// Don't do thisprocessBar(x);}functionf2() {constx=expressionReturningFoo();processBar(xasany);// Prefer this}
其中,第二种形式更为可取。为什么?因为该any类型的范围仅限于函数参数中的单个表达式。它在此参数或此行之外没有任何影响。如果processBar调用引用之后的代码x,它的类型仍然是Foo,并且它仍然能够触发类型错误,而在第一个示例中,它的类型是any直到它在函数结束时超出范围。
Of these, the second form is vastly preferable. Why? Because the any type is scoped to a single expression in a function argument. It has no effect outside this argument or this line. If code after the processBar call references x, its type will still be Foo, and it will still be able to trigger type errors, whereas in the first example its type is any until it goes out of scope at the end of the function.
如果您从此函数返回, 风险会显着增加。x看看会发生什么:
The stakes become significantly higher if you return x from this function. Look what happens:
functionf1() {constx:any=expressionReturningFoo();processBar(x);returnx;}functiong() {constfoo=f1();// Type is anyfoo.fooMethod();// This call is unchecked!}
functionf1() {constx:any=expressionReturningFoo();processBar(x);returnx;}functiong() {constfoo=f1();// Type is anyfoo.fooMethod();// This call is unchecked!}
返回类型any具有“传染性”,因为它可以在整个代码库中传播。由于我们的改变f,一种any类型悄然出现了g。any对于范围更窄的in ,这不会发生f2。
An any return type is “contagious” in that it can spread throughout a codebase. As a result of our changes to f, an any type has quietly appeared in g. This would not have happened with the more narrowly scoped any in f2.
(这是考虑包括显式返回类型注释的一个很好的理由,即使可以推断返回类型。它防止any类型“转义”。请参阅第 19 项中的讨论。)
(This is a good reason to consider including explicit return type annotations, even when the return type can be inferred. It prevents an any type from “escaping.” See discussion in Item 19.)
我们此处用于any消除我们认为不正确的错误。另一种方法是使用@ts-ignore:
We used any here to silence an error that we believed to be incorrect. Another way to do this is with @ts-ignore:
functionf1() {constx=expressionReturningFoo();// @ts-ignoreprocessBar(x);returnx;}
functionf1() {constx=expressionReturningFoo();// @ts-ignoreprocessBar(x);returnx;}
这会消除下一行的错误,保持类型x不变。尽量不要过分依赖@ts-ignore:类型检查器通常有充分的理由抱怨。这也意味着,如果下一行的错误变成了更有问题的东西,您将不会知道。
This silences an error on the next line, leaving the type of x unchanged. Try not to lean too heavily on @ts-ignore: the type checker usually has a good reason to complain. It also means that if the error on the next line changes to something more problematic, you won’t know.
您还可能会遇到这样的情况,即在一个较大的对象中,只有一个属性会出现类型错误:
You may also run into situations where you get a type error for just one property in a larger object:
constconfig::::Config={a_1,b_2,c:{key_value// ~~~ Property ... missing in type 'Bar' but required in type 'Foo'}};
constconfig:Config={a:1,b:2,c:{key:value// ~~~ Property ... missing in type 'Bar' but required in type 'Foo'}};
as any您可以通过在整个对象周围抛出一个来消除这样的错误config:
You can silence errors like this by throwing an as any around the whole config object:
constconfig::::Config={a_1,b_2,c:{key_value}}asany;// Don't do this!
constconfig:Config={a:1,b:2,c:{key:value}}asany;// Don't do this!
a但这也有禁用其他属性 (和)类型检查的副作用b。使用范围更窄的any限制损害:
But this has the side effect of disabling type checking for the other properties (a and b) as well. Using a more narrowly scoped any limits the damage:
constconfig::::Config={a_1,b_2,// These properties are still checkedc:{key_valueasany}};
constconfig:Config={a:1,b:2,// These properties are still checkedc:{key:valueasany}};
any尽可能缩小您对 use 的使用范围,以避免在代码的其他地方意外丢失类型安全。
Make your uses of any as narrowly scoped as possible to avoid undesired loss of type safety elsewhere in your code.
any永远不要从函数返回类型。这将悄无声息地导致任何调用该函数的客户端失去类型安全。
Never return an any type from a function. This will silently lead to the loss of type safety for any client calling the function.
Consider @ts-ignore as an alternative to any if you need to silence one error.
这 any类型包含所有可以用 JavaScript 表达的值。这是一个庞大的集合!它不仅包括所有数字和字符串,还包括所有数组、对象、正则表达式、函数、类和 DOM 元素,更不用说null和undefined。当你使用any类型时,问问你是否真的有更具体的想法。传入正则表达式或函数是否可以?
The any type encompasses all values that can be expressed in JavaScript. This is a vast set! It includes not just all numbers and strings, but all arrays, objects, regular expressions, functions, classes, and DOM elements, not to mention null and undefined. When you use an any type, ask whether you really had something more specific in mind. Would it be OK to pass in a regular expression or a function?
通常答案是“否”,在这种情况下,您可以通过使用更具体的类型来保持某种类型安全:
Often the answer is “no,” in which case you might be able to retain some type safety by using a more specific type:
functiongetLengthBad(array:any){// Don't do this!returnarray.length;}functiongetLength(array:any[]){returnarray.length;}
functiongetLengthBad(array:any){// Don't do this!returnarray.length;}functiongetLength(array:any[]){returnarray.length;}
后一个版本使用any[]代替any,在三个方面更好:
The latter version, which uses any[] instead of any, is better in three ways:
array.length对函数体中的引用进行了类型检查。
The reference to array.length in the function body is type checked.
函数的返回类型被推断为 而number不是any。
The function’s return type is inferred as number instead of any.
将检查调用getLength以确保参数是一个数组:
Calls to getLength will be checked to ensure that the parameter is an array:
getLengthBad(/123/);// No error, returns undefinedgetLength(/123/);// ~~~~~ Argument of type 'RegExp' is not assignable// to parameter of type 'any[]'
getLengthBad(/123/);// No error, returns undefinedgetLength(/123/);// ~~~~~ Argument of type 'RegExp' is not assignable// to parameter of type 'any[]'
如果您希望参数是数组的数组但不关心类型,您可以使用any[][]. 如果您期望某种对象但不知道值是什么,您可以使用{[key: string]: any}:
If you expect a parameter to be an array of arrays but don’t care about the type, you can use any[][]. If you expect some sort of object but don’t know what the values will be, you can use {[key: string]: any}:
functionhasTwelveLetterKey(o:{[key:string]:any}){for(constkeyino){if(key.length===12){returntrue;}}returnfalse;}
functionhasTwelveLetterKey(o:{[key:string]:any}){for(constkeyino){if(key.length===12){returntrue;}}returnfalse;}
您也可以在这种情况下使用object类型,它包括所有非原始类型。这略有不同,虽然您仍然可以枚举键,但您无法访问其中任何一个的值:
You could also use the object type in this situation, which includes all non-primitive types. This is slightly different in that, while you can still enumerate keys, you can’t access the values of any of them:
functionhasTwelveLetterKey(o:object){for(constkeyino){if(key.length===12){console.log(key,o[key]);// ~~~~~~ Element implicitly has an 'any' type// because type '{}' has no index signaturereturntrue;}}returnfalse;}
functionhasTwelveLetterKey(o:object){for(constkeyino){if(key.length===12){console.log(key,o[key]);// ~~~~~~ Element implicitly has an 'any' type// because type '{}' has no index signaturereturntrue;}}returnfalse;}
如果这种类型符合您的需要,您可能也会对该unknown类型感兴趣。请参阅第 42 项。
If this sort of type fits your needs, you might also be interested in the unknown type. See Item 42.
any如果您需要函数类型,请避免使用。您在这里有几个选项,具体取决于您想要获得的具体程度:
Avoid using any if you expect a function type. You have several options here depending on how specific you want to get:
typeFn0=()=>any;// any function callable with no paramstypeFn1=(arg:any)=>any;// With one paramtypeFnN=(...args:any[])=>any;// With any number of params// same as "Function" type
typeFn0=()=>any;// any function callable with no paramstypeFn1=(arg:any)=>any;// With one paramtypeFnN=(...args:any[])=>any;// With any number of params// same as "Function" type
所有这些都比any它更精确,因此更可取。请注意在上一个示例中将 用作any[]其余参数的类型。any也可以在这里工作,但不太精确:
All of these are more precise than any and hence preferable to it. Note the use of any[] as the type for the rest parameter in the last example. any would also work here but would be less precise:
constnumArgsBad=(...args:any)=>args.length;// Returns anyconstnumArgsGood=(...args:any[])=>args.length;// Returns number
constnumArgsBad=(...args:any)=>args.length;// Returns anyconstnumArgsGood=(...args:any[])=>args.length;// Returns number
这可能是该类型最常见的用法any[]。
This is perhaps the most common use of the any[] type.
当您使用 时any,请考虑是否有任何 JavaScript 值是真正允许的。
When you use any, think about whether any JavaScript value is truly permissible.
如果它们更准确地为您的数据建模,则更喜欢更精确的形式,any例如any[]or{[id: string]: any}或 or 。() => any
Prefer more precise forms of any such as any[] or {[id: string]: any} or () => any if they more accurately model your data.
那里许多函数的类型签名很容易编写,但其实现很难用类型安全代码编写。虽然编写类型安全的实现是一个崇高的目标,但处理您知道不会出现在您的代码中的边缘情况可能并不值得。如果对类型安全实现的合理尝试不起作用,请使用隐藏在具有正确类型签名的函数内的不安全类型断言。隐藏在类型良好的函数中的不安全断言比散布在代码中的不安全断言要好得多。
There are many functions whose type signatures are easy to write but whose implementations are quite difficult to write in type-safe code. And while writing type-safe implementations is a noble goal, it may not be worth the difficulty to deal with edge cases that you know don’t come up in your code. If a reasonable attempt at a type-safe implementation doesn’t work, use an unsafe type assertion hidden inside a function with the right type signature. Unsafe assertions hidden inside well-typed functions are much better than unsafe assertions scattered throughout your code.
认为你想让一个函数缓存它的最后一次调用。这是一种常用技术,用于消除 React 等框架的昂贵函数调用。1cacheLast编写一个将此行为添加到任何函数的通用包装器会很好。它的声明很容易写:
Suppose you want to make a function cache its last call. This is a common technique for eliminating expensive function calls with frameworks like React.1 It would be nice to write a general cacheLast wrapper that adds this behavior to any function. Its declaration is easy to write:
declarefunctioncacheLast<TextendsFunction>(fn:T):T;
declarefunctioncacheLast<TextendsFunction>(fn:T):T;
这是一个实现的尝试:
Here’s an attempt at an implementation:
functioncacheLast<TextendsFunction>(fn::::T):T{letlastArgs_any[]|null=null;letlastResult_any;returnfunction(...args_any[]){// ~~~~~~~~~~~~~~~~~~~~~~~~~~// Type '(...args: any[]) => any' is not assignable to type 'T'if(!lastArgs||!shallowEqual(lastArgs,args)){lastResult=fn(...args);lastArgs=args;}returnlastResult;};}
functioncacheLast<TextendsFunction>(fn:T):T{letlastArgs:any[]|null=null;letlastResult:any;returnfunction(...args:any[]){// ~~~~~~~~~~~~~~~~~~~~~~~~~~// Type '(...args: any[]) => any' is not assignable to type 'T'if(!lastArgs||!shallowEqual(lastArgs,args)){lastResult=fn(...args);lastArgs=args;}returnlastResult;};}
这个错误是有道理的:TypeScript 没有理由相信这个非常松散的函数与T. 但是您知道类型系统会强制使用正确的参数调用它,并且它的返回值被赋予正确的类型。因此,如果您在此处添加类型断言,您不应该期望出现太多问题:
The error makes sense: TypeScript has no reason to believe that this very loose function has any relation to T. But you know that the type system will enforce that it’s called with the right parameters and that its return value is given the correct type. So you shouldn’t expect too many problems if you add a type assertion here:
functioncacheLast<TextendsFunction>(fn::::T):T{letlastArgs_any[]|null=null;letlastResult_any;returnfunction(...args_any[]){if(!lastArgs||!shallowEqual(lastArgs,args)){lastResult=fn(...args);lastArgs=args;}returnlastResult;}asunknownasT;}
functioncacheLast<TextendsFunction>(fn:T):T{letlastArgs:any[]|null=null;letlastResult:any;returnfunction(...args:any[]){if(!lastArgs||!shallowEqual(lastArgs,args)){lastResult=fn(...args);lastArgs=args;}returnlastResult;}asunknownasT;}
事实上,这对于您传递给它的任何简单函数都非常有用。此实现中隐藏了相当多的any类型,但您已将它们排除在类型签名之外,因此调用的代码cacheLast不会更明智。
And indeed this will work great for any simple function you pass it. There are quite a few any types hidden in this implementation, but you’ve kept them out of the type signature, so the code that calls cacheLast will be none the wiser.
(这真的安全吗?这个实现有几个真正的问题:它不检查this连续调用的值是否相同。如果原始函数定义了属性,那么包装函数就不会这些,所以它不会有相同的类型。但是如果你知道这些情况不会出现在你的代码中,这个实现就很好。这个函数可以用类型安全的方式编写,但它是一个留给读者的更复杂的练习。)
(Is this actually safe? There are a few real problems with this implementation: it doesn’t check that the values of this for successive calls are the same. And if the original function had properties defined on it, then the wrapped function would not have these, so it wouldn’t have the same type. But if you know that these situations don’t come up in your code, this implementation is just fine. This function can be written in a type-safe way, but it is a more complex exercise that is left to the reader.)
前面示例中的函数shallowEqual对两个数组进行操作,易于输入和实现。但是对象的变化更有趣。与 一样cacheLast,很容易编写其类型签名:
The shallowEqual function from the previous example operated on two arrays and is easy to type and implement. But the object variation is more interesting. As with cacheLast, it’s easy to write its type signature:
declarefunctionshallowObjectEqual<Textendsobject>(a:T,b:T):boolean;
declarefunctionshallowObjectEqual<Textendsobject>(a:T,b:T):boolean;
这实现需要一些小心,因为不能保证a和b具有相同的密钥(参见条款 54):
The implementation requires some care since there’s no guarantee that a and b have the same keys (see Item 54):
functionshallowObjectEqual<Textendsobject>(a:T,b:T):boolean{for(const[k,aVal]ofObject.entries(a)){if(!(kinb)||aVal!==b[k]){// ~~~~ Element implicitly has an 'any' type// because type '{}' has no index signaturereturnfalse;}}returnObject.keys(a).length===Object.keys(b).length;}
functionshallowObjectEqual<Textendsobject>(a:T,b:T):boolean{for(const[k,aVal]ofObject.entries(a)){if(!(kinb)||aVal!==b[k]){// ~~~~ Element implicitly has an 'any' type// because type '{}' has no index signaturereturnfalse;}}returnObject.keys(a).length===Object.keys(b).length;}
有点令人惊讶的是,TypeScript 抱怨访问权限,b[k]尽管您刚刚检查过这k in b是真的。但确实如此,所以你别无选择,只能投:
It’s a bit surprising that TypeScript complains about the b[k] access despite your having just checked that k in b is true. But it does, so you have no choice but to cast:
functionshallowObjectEqual<Textendsobject>(a:T,b:T):boolean{for(const[k,aVal]ofObject.entries(a)){if(!(kinb)||aVal!==(basany)[k]){returnfalse;}}returnObject.keys(a).length===Object.keys(b).length;}
functionshallowObjectEqual<Textendsobject>(a:T,b:T):boolean{for(const[k,aVal]ofObject.entries(a)){if(!(kinb)||aVal!==(basany)[k]){returnfalse;}}returnObject.keys(a).length===Object.keys(b).length;}
这种类型断言是无害的(因为您已经检查过k in b),并且您得到了具有清晰类型签名的正确函数。这比分散迭代和断言来检查整个代码中的对象相等性要好得多!
This type assertion is harmless (since you’ve checked k in b), and you’re left with a correct function with a clear type signature. This is much preferable to scattering iteration and assertions to check for object equality throughout your code!
有时不安全的类型断言是必要的或权宜之计。当您需要使用一个时,将其隐藏在具有正确签名的函数中。
Sometimes unsafe type assertions are necessary or expedient. When you need to use one, hide it inside a function with a correct signature.
在TypeScript 变量的类型通常在声明时确定。在此之后,可以对其进行细化null(例如,通过检查它是否为),但不能扩展以包含新值。然而,有一个值得注意的例外,涉及any类型。
In TypeScript a variable’s type is generally determined when it is declared. After this, it can be refined (by checking if it is null, for instance), but it cannot expand to include new values. There is one notable exception to this, however, involving any types.
在 JavaScript 中,您可能会编写一个函数来生成一系列数字,如下所示:
In JavaScript, you might write a function to generate a range of numbers like this:
functionrange(start,limit){constout=[];for(leti=start;i<limit;i++){out.push(i);}returnout;}
functionrange(start,limit){constout=[];for(leti=start;i<limit;i++){out.push(i);}returnout;}
当您将其转换为 TypeScript 时,它会完全按照您的预期工作:
When you convert this to TypeScript, it works exactly as you’d expect:
functionrange(start:number,limit:number){constout=[];for(leti=start;i<limit;i++){out.push(i);}returnout;// Return type inferred as number[]}
functionrange(start:number,limit:number){constout=[];for(leti=start;i<limit;i++){out.push(i);}returnout;// Return type inferred as number[]}
然而,经过仔细检查,这竟然有效!当初始化为 时, TypeScript 是如何知道它的类型的out,它可以是任何类型的数组?number[][]
Upon closer inspection, however, it’s surprising that this works! How does TypeScript know that the type of out is number[] when it’s initialized as [], which could be an array of any type?
检查三个出现的每一个out以揭示其推断类型开始讲述这个故事:
Inspecting each of the three occurrences of out to reveal its inferred type starts to tell the story:
functionrange(start:number,limit:number){constout=[];// Type is any[]for(leti=start;i<limit;i++){out.push(i);// Type of out is any[]}returnout;// Type is number[]}
functionrange(start:number,limit:number){constout=[];// Type is any[]for(leti=start;i<limit;i++){out.push(i);// Type of out is any[]}returnout;// Type is number[]}
的类型out开始为any[],一个未区分的数组。但是当我们将number值压入它时,它的类型“进化”成为number[].
The type of out starts as any[], an undifferentiated array. But as we push number values onto it, its type “evolves” to become number[].
这与缩小(项目 22 )不同。数组的类型可以通过将不同的元素压入其中来扩展:
This is distinct from narrowing (Item 22). An array’s type can expand by pushing different elements onto it:
constresult=[];// Type is any[]result.push('a');result// Type is string[]result.push(1);result// Type is (string | number)[]
constresult=[];// Type is any[]result.push('a');result// Type is string[]result.push(1);result// Type is (string | number)[]
对于条件语句,类型甚至可以在不同分支中变化。这里我们用一个简单的值而不是一个数组来展示相同的行为:
With conditionals, the type can even vary across branches. Here we show the same behavior with a simple value, rather than an array:
letval;// Type is anyif(Math.random()<0.5){val=/hello/;val// Type is RegExp}else{val=12;val// Type is number}val// Type is number | RegExp
letval;// Type is anyif(Math.random()<0.5){val=/hello/;val// Type is RegExp}else{val=12;val// Type is number}val// Type is number | RegExp
触发这种“进化任何”行为的最后一种情况是变量最初是null. try当您在/块中设置值时,通常会出现这种情况catch:
A final case that triggers this “evolving any” behavior is if a variable is initially null. This often comes up when you set a value in a try/catch block:
letval=null;// Type is anytry{somethingDangerous();val=12;val// Type is number}catch(e){console.warn('alas!');}val// Type is number | null
letval=null;// Type is anytry{somethingDangerous();val=12;val// Type is number}catch(e){console.warn('alas!');}val// Type is number | null
有趣的是,只有当变量的类型隐式设置时才会发生这种any行为noImplicitAny!添加显式 any保持类型不变:
Interestingly, this behavior only happens when a variable’s type is implicitly any with noImplicitAny set! Adding an explicit any keeps the type constant:
letval:any;// Type is anyif(Math.random()<0.5){val=/hello/;val// Type is any}else{val=12;val// Type is any}val// Type is any
letval:any;// Type is anyif(Math.random()<0.5){val=/hello/;val// Type is any}else{val=12;val// Type is any}val// Type is any
在您的编辑器中遵循这种行为可能会造成混淆,因为类型仅在您分配或推送元素后才“进化”。检查分配行上的类型仍将显示anyor any[]。
This behavior can be confusing to follow in your editor since the type is only “evolved” after you assign or push an element. Inspecting the type on the line with the assignment will still show any or any[].
如果您在对它进行任何赋值之前使用一个值,您将得到一个隐含的任何错误:
If you use a value before any assignment to it, you’ll get an implicit any error:
functionrange(start:number,limit:number){constout=[];// ~~~ Variable 'out' implicitly has type 'any[]' in some// locations where its type cannot be determinedif(start===limit){returnout;// ~~~ Variable 'out' implicitly has an 'any[]' type}for(leti=start;i<limit;i++){out.push(i);}returnout;}
functionrange(start:number,limit:number){constout=[];// ~~~ Variable 'out' implicitly has type 'any[]' in some// locations where its type cannot be determinedif(start===limit){returnout;// ~~~ Variable 'out' implicitly has an 'any[]' type}for(leti=start;i<limit;i++){out.push(i);}returnout;}
换句话说,“进化的”any类型只有any当你写给它们时才会出现。如果你试图在它们静止时读取any它们,你会得到一个错误。
Put another way, “evolving” any types are only any when you write to them. If you try to read from them while they’re still any, you’ll get an error.
隐式any类型不会通过函数调用演变。这里的箭头函数绊倒了推理:
Implicit any types do not evolve through function calls. The arrow function here trips up inference:
functionmakeSquares(start:number,limit:number){constout=[];// ~~~ Variable 'out' implicitly has type 'any[]' in some locationsrange(start,limit).forEach(i=>{out.push(i*i);});returnout;// ~~~ Variable 'out' implicitly has an 'any[]' type}
functionmakeSquares(start:number,limit:number){constout=[];// ~~~ Variable 'out' implicitly has type 'any[]' in some locationsrange(start,limit).forEach(i=>{out.push(i*i);});returnout;// ~~~ Variable 'out' implicitly has an 'any[]' type}
在这种情况下,您可能需要考虑使用数组的map和filter方法在单个语句中构建数组并避免迭代和any完全进化。请参阅第23和27项。
In cases like this, you may want to consider using an array’s map and filter methods to build arrays in a single statement and avoid iteration and evolving any entirely. See Items 23 and 27.
进化any伴随着所有关于类型推断的常见警告。您的数组的类型真的正确吗(string|number)[]?或者它应该是number[]你错误地推了一个string?您可能仍然希望提供显式类型注释以获得更好的错误检查,而不是使用 evolving any。
Evolving any comes with all the usual caveats about type inference. Is the correct type for your array really (string|number)[]? Or should it be number[] and you incorrectly pushed a string? You may still want to provide an explicit type annotation to get better error checking instead of using evolving any.
虽然 TypeScript 类型通常只精炼,但允许隐式any和类型演化。你应该能够识别和理解这个结构出现的地方。any[]
While TypeScript types typically only refine, implicit any and any[] types are allowed to evolve. You should be able to recognize and understand this construct where it occurs.
For better error checking, consider providing an explicit type annotation instead of using evolving any.
认为你想写一个YAML 解析器(YAML 可以表示与JSON 但允许 JSON 语法的超集)。你的方法的返回类型应该parseYAML是什么?制作它很诱人any(例如JSON.parse):
Suppose you want to write a YAML parser (YAML can represent the same set of values as JSON but allows a superset of JSON’s syntax). What should the return type of your parseYAML method be? It’s tempting to make it any (like JSON.parse):
functionparseYAML(yaml:string):any{// ...}
functionparseYAML(yaml:string):any{// ...}
但这违背了Item 38关于避免“传染”any类型的建议,特别是不从函数中返回它们。
But this flies in the face of Item 38’s advice to avoid “contagious” any types, specifically by not returning them from functions.
理想情况下,您希望您的用户立即将结果分配给另一种类型:
Ideally you’d like your users to immediately assign the result to another type:
interfaceBook{name:string;author:string;}constbook:Book=parseYAML(`name: Wuthering Heightsauthor: Emily Brontë`);
interfaceBook{name:string;author:string;}constbook:Book=parseYAML(`name: Wuthering Heightsauthor: Emily Brontë`);
但是,如果没有类型声明,该book变量将悄悄地获得一个any类型,从而阻碍在任何使用它的地方进行类型检查:
Without the type declarations, though, the book variable would quietly get an any type, thwarting type checking wherever it’s used:
constbook=parseYAML(`name: Jane Eyreauthor: Charlotte Brontë`);alert(book.title);// No error, alerts "undefined" at runtimebook('read');// No error, throws "TypeError: book is not a// function" at runtime
constbook=parseYAML(`name: Jane Eyreauthor: Charlotte Brontë`);alert(book.title);// No error, alerts "undefined" at runtimebook('read');// No error, throws "TypeError: book is not a// function" at runtime
一个更安全的选择是返回parseYAML一个unknown类型:
A safer alternative would be to have parseYAML return an unknown type:
functionsafeParseYAML(yaml:string):unknown{returnparseYAML(yaml);}constbook=safeParseYAML(`name: The Tenant of Wildfell Hallauthor: Anne Brontë`);alert(book.title);// ~~~~ Object is of type 'unknown'book("read");// ~~~~~~~~~~ Object is of type 'unknown'
functionsafeParseYAML(yaml:string):unknown{returnparseYAML(yaml);}constbook=safeParseYAML(`name: The Tenant of Wildfell Hallauthor: Anne Brontë`);alert(book.title);// ~~~~ Object is of type 'unknown'book("read");// ~~~~~~~~~~ Object is of type 'unknown'
要理解类型,从可分配性的角度unknown考虑会有所帮助。any的力量和危险any来自两个属性:
To understand the unknown type, it helps to think about any in terms of assignability. The power and danger of any come from two properties:
任何类型都可以分配给该any类型。
Any type is assignable to the any type.
该any类型可分配给任何其他类型。2个
The any type is assignable to any other type.2
在“将类型视为值集”(第 7 项)的上下文中,any显然不适合类型系统,因为一个集不能同时是所有其他集的子集和超集。这是 的力量来源any,但也是它有问题的原因。由于类型检查器是基于集合的,因此使用any有效地禁用了它。
In the context of “thinking of types as sets of values” (Item 7), any clearly doesn’t fit into the type system, since a set can’t simultaneously be both a subset and a superset of all other sets. This is the source of any’s power but also the reason it’s problematic. Since the type checker is set-based, the use of any effectively disables it.
该类型是适合类型系统的替代unknown方案。它具有第一个属性(任何类型都可以分配给),但没有第二个(只能分配给并且,当然,)。这anyunknownunknownunknownany nevertype 则相反:它具有第二个属性(可以分配给任何其他类型)但不是第一个(没有任何东西可以分配给never)。
The unknown type is an alternative to any that does fit into the type system. It has the first property (any type is assignable to unknown) but not the second (unknown is only assignable to unknown and, of course, any). The never type is the opposite: it has the second property (can be assigned to any other type) but not the first (nothing can be assigned to never).
尝试访问具有该类型的值的属性unknown是错误的。尝试调用它或对其进行算术运算也是如此。你不能用 做很多事情unknown,这正是重点。有关类型的错误unknown将鼓励您添加适当的类型:
Attempting to access a property on a value with the unknown type is an error. So is attempting to call it or do arithmetic with it. You can’t do much with unknown, which is exactly the point. The errors about an unknown type will encourage you to add an appropriate type:
constbook=safeParseYAML(`name: Villetteauthor: Charlotte Brontë`)asBook;alert(book.title);// ~~~~~ Property 'title' does not exist on type 'Book'book('read');// ~~~~~~~~~ this expression is not callable
constbook=safeParseYAML(`name: Villetteauthor: Charlotte Brontë`)asBook;alert(book.title);// ~~~~~ Property 'title' does not exist on type 'Book'book('read');// ~~~~~~~~~ this expression is not callable
这些错误更明智。由于unknown不可分配给其他类型,因此需要类型断言。但这也很合适:我们确实比 TypeScript 更了解结果对象的类型。
These errors are more sensible. Since unknown is not assignable to other types, a type assertion is required. But it is also appropriate: we really do know more about the type of the resulting object than TypeScript does.
unknown当您知道会有一个值但不知道其类型时,它是合适的。结果parseYAML是一个例子,但还有其他例子。在GeoJSON 规范,例如,propertiesFeature 的属性是任何东西的抓包JSON 可序列化。所以unknown有道理:
unknown is appropriate whenever you know that there will be a value but you don’t know its type. The result of parseYAML is one example, but there are others. In the GeoJSON spec, for example, the properties property of a Feature is a grab-bag of anything JSON serializable. So unknown makes sense:
interfaceFeature{id?:string|number;geometry:Geometry;properties:unknown;}
interfaceFeature{id?:string|number;geometry:Geometry;properties:unknown;}
类型断言不是从unknown对象中恢复类型的唯一方法。一张instanceof支票会做:
A type assertion isn’t the only way to recover a type from an unknown object. An instanceof check will do:
functionprocessValue(val:unknown){if(valinstanceofDate){val// Type is Date}}
functionprocessValue(val:unknown){if(valinstanceofDate){val// Type is Date}}
您还可以使用用户定义的类型保护:
You can also use a user-defined type guard:
functionisBook(val:unknown):valisBook{return(typeof(val)==='object'&&val!==null&&'name'inval&&'author'inval);}functionprocessValue(val:unknown){if(isBook(val)){val;// Type is Book}}
functionisBook(val:unknown):valisBook{return(typeof(val)==='object'&&val!==null&&'name'inval&&'author'inval);}functionprocessValue(val:unknown){if(isBook(val)){val;// Type is Book}}
TypeScript 需要相当多的证据来缩小类型unknown:为了避免检查错误in,您首先必须证明它val是一个对象类型并且它不是 - null(since typeof null === 'object')。
TypeScript requires quite a bit of proof to narrow an unknown type: in order to avoid errors on the in checks, you first have to demonstrate that val is an object type and that it is non-null (since typeof null === 'object').
有时您会看到使用通用参数而不是unknown. 您可以safeParseYAML这样声明函数:
You’ll sometimes see a generic parameter used instead of unknown. You could have declared the safeParseYAML function this way:
functionsafeParseYAML<T>(yaml:string):T{returnparseYAML(yaml);}
functionsafeParseYAML<T>(yaml:string):T{returnparseYAML(yaml);}
然而,这在 TypeScript 中通常被认为是糟糕的风格。它看起来与类型断言不同,但在功能上是相同的。最好只返回unknown并强制您的用户使用断言或缩小到他们想要的类型。
This is generally considered bad style in TypeScript, however. It looks different than a type assertion, but is functionally the same. Better to just return unknown and force your users to use an assertion or narrow to the type they want.
unknown can also be used instead of any in “double assertions”:
declareconstfoo:Foo;letbarAny=fooasanyasBar;letbarUnk=fooasunknownasBar;
declareconstfoo:Foo;letbarAny=fooasanyasBar;letbarUnk=fooasunknownasBar;
它们在功能上是等效的,但unknown如果您进行重构并分解这两个断言,则该表单的风险较小。在那种情况下,病毒any可能会逃逸并扩散。如果unknown类型转义,它可能只会产生错误。
These are functionally equivalent, but the unknown form has less risk if you do a refactor and break up the two assertions. In that case the any could escape and spread. If the unknown type escapes, it will probably just produce an error.
最后一点,您可能会看到使用object或以与本项目中描述的{}方式类似的方式使用的代码。unknown它们也是广泛的类型,但比unknown:
As a final note, you may see code that uses object or {} in a similar way to how unknown has been described in this item. They are also broad types but are slightly narrower than unknown:
该类型由除和{}之外的所有值组成。nullundefined
The {} type consists of all values except null and undefined.
该object类型由所有非原始类型组成。这不包括trueor12或"foo"但确实包括对象和数组。
The object type consists of all non-primitive types. This doesn’t include true or 12 or "foo" but does include objects and arrays.
在引入类型之前,使用{}更常见。unknown今天的使用有些罕见:只有在您确实知道并且不可能的情况{}下才使用而不是。unknownnullundefined
The use of {} was more common before the unknown type was introduced. Uses today are somewhat rare: only use {} instead of unknown if you really do know that null and undefined aren’t possibilities.
该unknown类型是 的类型安全替代方案any。当你知道你有一个值但不知道它的类型是什么时使用它。
The unknown type is a type-safe alternative to any. Use it when you know you have a value but do not know what its type is.
用于unknown强制您的用户使用类型断言或进行类型检查。
Use unknown to force your users to use a type assertion or do type checking.
一JavaScript 最著名的特性之一是它的对象和类是“开放的”,因为您可以向它们添加任意属性。window这偶尔用于通过分配给或来在网页上创建全局变量document:
One of the most famous features of JavaScript is that its objects and classes are “open” in the sense that you can add arbitrary properties to them. This is occasionally used to create global variables on web pages by assigning to window or document:
window.monkey='Tamarin';document.monkey='Howler';
window.monkey='Tamarin';document.monkey='Howler';
或者将数据附加到 DOM 元素:
or to attach data to DOM elements:
constel=document.getElementById('colobus');el.home='tree';
constel=document.getElementById('colobus');el.home='tree';
This style is particularly common with code that uses jQuery.
您甚至可以将属性附加到内置函数的原型上,有时会产生令人惊讶的结果:
You can even attach properties to the prototypes of built-ins, with sometimes surprising results:
>RegExp.prototype.monkey='Capuchin'"Capuchin">/123/.monkey"Capuchin"
>RegExp.prototype.monkey='Capuchin'"Capuchin">/123/.monkey"Capuchin"
这些方法通常不是好的设计。当您将数据附加到windowDOM 节点或 DOM 节点时,您实际上是在将它变成一个全局变量。这使得很容易在不经意间引入程序中相距遥远的部分之间的依赖关系,并且意味着无论何时调用函数都必须考虑副作用。
These approaches are generally not good designs. When you attach data to window or a DOM node, you are essentially turning it into a global variable. This makes it easy to inadvertently introduce dependencies between far-flung parts of your program and means that you have to think about side effects whenever you call a function.
Document添加 TypeScript 会引入另一个问题:虽然类型检查器知道和的内置属性HTMLElement,但它肯定不知道您添加的属性:
Adding TypeScript introduces another problem: while the type checker knows about built-in properties of Document and HTMLElement, it certainly doesn’t know about the ones you’ve added:
document.monkey='Tamarin';// ~~~~~~ Property 'monkey' does not exist on type 'Document'
document.monkey='Tamarin';// ~~~~~~ Property 'monkey' does not exist on type 'Document'
修复此错误的最直接方法是使用断言any:
The most straightforward way to fix this error is with an any assertion:
(documentasany).monkey='Tamarin';// OK
(documentasany).monkey='Tamarin';// OK
这满足了类型检查器的要求,但是,现在应该不足为奇了,它有一些缺点。与对 的任何使用一样any,您将失去类型安全和语言服务:
This satisfies the type checker, but, as should be no surprise by now, it has some downsides. As with any use of any, you lose type safety and language services:
(documentasany).monky='Tamarin';// Also OK, misspelled(documentasany).monkey=/Tamarin/;// Also OK, wrong type
(documentasany).monky='Tamarin';// Also OK, misspelled(documentasany).monkey=/Tamarin/;// Also OK, wrong type
最好的解决方案是将数据移出documentDOM。但是,如果您不能(也许您正在使用需要它的库或正在迁移 JavaScript 应用程序),那么您有一些次佳的选择可用。
The best solution is to move your data out of document or the DOM. But if you can’t (perhaps you’re using a library that requires it or are in the process of migrating a JavaScript application), then you have a few next-best options available.
一种是使用增强,这是interface(项目 13)的特殊能力之一:
One is to use an augmentation, one of the special abilities of interface (Item 13):
interfaceDocument{/** Genus or species of monkey patch */monkey:string;}document.monkey='Tamarin';// OK
interfaceDocument{/** Genus or species of monkey patch */monkey:string;}document.monkey='Tamarin';// OK
any这是对以下几种使用方式的改进:
This is an improvement over using any in a few ways:
你得到了类型安全。类型检查器将标记错误类型的拼写错误或分配。
You get type safety. The type checker will flag misspellings or assignments of the wrong type.
您可以将文档附加到属性(第 48 项)。
You can attach documentation to the property (Item 48).
您会在属性上获得自动完成功能。
You get autocomplete on the property.
有关于猴子补丁的确切内容的记录。
There is a record of precisely what the monkey patch is.
import在模块上下文中(即使用/的 TypeScript 文件export),您需要添加 adeclare global以使其工作:
In a module context (i.e., a TypeScript file that uses import / export), you’ll need to add a declare global to make this work:
export{};declareglobal{interfaceDocument{/** Genus or species of monkey patch */monkey:string;}}document.monkey='Tamarin';// OK
export{};declareglobal{interfaceDocument{/** Genus or species of monkey patch */monkey:string;}}document.monkey='Tamarin';// OK
使用扩充的主要问题与范围有关。首先,扩充适用于全球。您无法从代码的其他部分或库中隐藏它。其次,如果您在应用程序运行时分配属性,则无法仅在发生这种情况后才引入扩充。当您修补 HTML 元素时,这尤其成问题,页面上的某些元素将具有该属性,而另一些则不会。因此,您可能希望将该属性声明为string|undefined. 这更准确,但会使类型使用起来不太方便。
The main issues with using an augmentation have to do with scope. First, the augmentation applies globally. You can’t hide it from other parts of your code or from libraries. And second, if you assign the property while your application is running, there’s no way to introduce the augmentation only after this has happened. This is particularly problematic when you patch HTML Elements, where some elements on the page will have the property and some will not. For this reason, you might want to declare the property to be string|undefined. This is more accurate, but will make the type less convenient to work with.
另一种方法是使用更精确的类型断言:
Another approach is to use a more precise type assertion:
interfaceMonkeyDocumentextendsDocument{/** Genus or species of monkey patch */monkey:string;}(documentasMonkeyDocument).monkey='Macaque';
interfaceMonkeyDocumentextendsDocument{/** Genus or species of monkey patch */monkey:string;}(documentasMonkeyDocument).monkey='Macaque';
TypeScript 可以使用类型断言,因为Document和MonkeyDocument共享属性(第 9 项)。你在作业中获得了类型安全。范围问题也更易于管理:没有Document类型的全局修改,只是引入了一个新类型(只有在导入时才在范围内)。每当您引用 monkey-patched 属性时,您都必须编写一个断言(或引入一个新变量)。但您可以将其视为重构为更结构化的东西的鼓励。猴子补丁应该不会太容易!
TypeScript is OK with the type assertion because Document and MonkeyDocument share properties (Item 9). And you get type safety in the assignment. The scope issues are also more manageable: there’s no global modification of the Document type, just the introduction of a new type (which is only in scope if you import it). You have to write an assertion (or introduce a new variable) whenever you reference the monkey-patched property. But you can take that as encouragement to refactor into something more structured. Monkey patching shouldn’t be too easy!
更喜欢结构化代码而不是将数据存储在全局变量或 DOM 中。
Prefer structured code to storing data in globals or on the DOM.
如果您必须在内置类型上存储数据,请使用一种类型安全方法(扩充或断言自定义接口)。
If you must store data on built-in types, use one of the type-safe approaches (augmentation or asserting a custom interface).
了解扩充的范围问题。
Understand the scoping issues of augmentations.
是any一旦为具有隐式类型的值添加类型注释并启用,您就可以避免与任何类型相关的问题了吗noImplicitAny?答案是不”; any类型仍然可以通过两种主要方式进入您的程序:
Are you safe from the problems associated with any types once you’ve added type annotations for values with implicit any types and enabled noImplicitAny? The answer is “no”; any types can still enter your program in two main ways:
any类型any types即使您遵循条款38和39的建议,使您的any类型既窄又具体,它们仍然是any类型。特别是,一旦您对它们进行索引,类型 likeany[]和{[key: string]: any}就变成了 plain ,并且生成的类型可以流过您的代码。anyany
Even if you follow the advice of Items 38 and 39, making your any types both narrow and specific, they remain any types. In particular, types like any[] and {[key: string]: any} become plain anys once you index into them, and the resulting any types can flow through your code.
这是特别隐蔽的,因为any来自声明文件的类型@types静默输入:即使您noImplicitAny启用了并且您从未输入过any,您仍然有any类型流过您的代码。
This is particularly insidious since any types from an @types declaration file enter silently: even though you have noImplicitAny enabled and you never typed any, you still have any types flowing through your code.
由于any类型会对类型安全和开发人员体验产生负面影响(第 5 项),因此最好在代码库中跟踪它们的数量。有很多方法可以做到这一点,包括type-coveragenpm 上的包:
Because of the negative effects any types can have on type safety and developer experience (Item 5), it’s a good idea to keep track of the number of them in your codebase. There are many ways to do this, including the type-coverage package on npm:
$ npx 类型覆盖 9985 / 10117 98.69%
$ npx type-coverage 9985 / 10117 98.69%
这意味着,在该项目的 10,117 个符号中,9,985 个 (98.69%) 的类型any不是any. 如果更改无意中引入了一种any类型并且它流经了您的代码,您将看到该百分比相应下降。
This means that, of the 10,117 symbols in this project, 9,985 (98.69%) had a type other than any or an alias to any. If a change inadvertently introduces an any type and it flows through your code, you’ll see a corresponding drop in this percentage.
在某些方面,这个百分比是一种记录你遵循本章其他项目建议的程度的方法。使用窄范围any将减少具有类型的符号数量any,因此将使用更具体的形式,如any[]. 以数字方式跟踪这一点可以帮助您确保事情只会随着时间的推移变得更好。
In some ways this percentage is a way of keeping score on how well you’ve followed the advice of the other items in this chapter. Using narrowly scoped any will reduce the number of symbols with any types, and so will using more specific forms like any[]. Tracking this numerically helps you make sure things only get better over time.
即使收集一次类型覆盖率信息也可以提供信息。type-coverage使用标志运行--detail将打印any代码中每种类型出现的位置:
Even collecting type coverage information once can be informative. Running type-coverage with the --detail flag will print where every any type occurs in your code:
$ npx 类型覆盖 -- 详细信息 路径/to/code.ts:1:10 getColumnInfo 路径/to/module.ts:7:1 pt2 ...
$ npx type-coverage --detail path/to/code.ts:1:10 getColumnInfo path/to/module.ts:7:1 pt2 ...
这些值得研究,因为它们可能会发现any您没有考虑过的 s 来源。让我们看几个例子。
These are worth investigating because they’re likely to turn up sources of anys that you hadn’t considered. Let’s look at a few examples.
显式any类型通常是您之前为权宜之计而做出的选择的结果。也许您遇到了一个您不想花时间解决的类型错误。或者可能是您尚未写出的类型。或者你可能只是匆忙。
Explicit any types are often the result of choices you made for expediency earlier on. Perhaps you were getting a type error that you didn’t want to take the time to sort out. Or maybe the type was one that you hadn’t written out yet. Or you might have just been in a rush.
类型断言any可以防止类型流动到它们原本应该流动的地方。也许您已经构建了一个处理表格数据的应用程序,并且需要一个单参数函数来构建某种列描述:
Type assertions with any can prevent types from flowing where they otherwise would. Perhaps you’ve built an application that works with tabular data and needed a single-parameter function that built up some kind of column description:
functiongetColumnInfo(name:string):any{returnutils.buildColumnInfo(appState.dataSchema,name);// Returns any}
functiongetColumnInfo(name:string):any{returnutils.buildColumnInfo(appState.dataSchema,name);// Returns any}
该函数在某个时候utils.buildColumnInfo返回。any提醒一下,您为函数添加了注释和明确的“:any”注释。
The utils.buildColumnInfo function returned any at some point. As a reminder, you added a comment and an explicit “: any” annotation to the function.
但是,在随后的几个月中,您还添加了一个类型 for ColumnInfo,并且utils.buildColumnInfo不再返回any。注释any现在丢弃了有价值的类型信息。摆脱它!
However, in the intervening months you’ve also added a type for ColumnInfo, and utils.buildColumnInfo no longer returns any. The any annotation is now throwing away valuable type information. Get rid of it!
第三方any类型可以有几种形式,但最极端的是给整个模块一个any类型:
Third-party any types can come in a few forms, but the most extreme is when you give an entire module an any type:
declaremodule'my-module';
declaremodule'my-module';
现在您可以从中导入任何内容my-module而不会出错。这些符号都有类型,如果您通过它们传递值,any将会导致更多类型:any
Now you can import anything from my-module without error. These symbols all have any types and will lead to more any types if you pass values through them:
import{someMethod,someSymbol}from'my-module';// OKconstpt1={x:1,y:2,};// type is {x: number, y: number}constpt2=someMethod(pt1,someSymbol);// OK, pt2's type is any
import{someMethod,someSymbol}from'my-module';// OKconstpt1={x:1,y:2,};// type is {x: number, y: number}constpt2=someMethod(pt1,someSymbol);// OK, pt2's type is any
由于用法看起来与类型良好的模块相同,因此很容易忘记您已删除该模块。或者也许是同事干的,而您一开始就不知道。值得不时地重新审视这些。也许模块有官方类型声明。或者您可能已经对该模块有了足够的了解,可以自己编写类型并将它们贡献回社区。
Since the usage looks identical to a well-typed module, it’s easy to forget that you stubbed out the module. Or maybe a coworker did it and you never knew in the first place. It’s worth revisiting these from time to time. Maybe there are official type declarations for the module. Or perhaps you’ve gained enough understanding of the module to write types yourself and contribute them back to the community.
any带有第三方声明的 s的另一个常见来源是类型中存在错误。也许声明没有遵循条款 29的建议,并声明了一个返回联合类型的函数,而实际上它返回的是更具体的东西。当您第一次使用该函数时,这似乎不值得修复,因此您使用了断言any。但也许从那时起声明就已经修复了。或者也许是时候自己修复它们了!
Another common source of anys with third-party declarations is when there’s a bug in the types. Maybe the declarations didn’t follow the advice of Item 29 and declared a function to return a union type when in fact it returns something much more specific. When you first used the function this didn’t seem worth fixing so you used an any assertion. But maybe the declarations have been fixed since then. Or maybe it’s time to fix them yourself!
导致您使用类型的注意事项any可能不再适用。也许现在可以在以前使用的地方插入一种类型any。也许不再需要不安全的类型断言。也许您正在解决的类型声明中的错误已得到修复。跟踪您的类型覆盖率会突出显示这些选择,并鼓励您继续重新访问它们。
The considerations that led you to use an any type might not apply any more. Maybe there’s a type you can plug in now where previously you used any. Maybe an unsafe type assertion is no longer necessary. Maybe the bug in the type declarations you were working around has been fixed. Tracking your type coverage highlights these choices and encourages you to keep revisiting them.
即使使用noImplicitAnyset,类型也可以通过显式或第三方类型声明 ( )any进入您的代码。any@types
Even with noImplicitAny set, any types can make their way into your code either through explicit anys or third-party type declarations (@types).
考虑跟踪您的程序的类型化程度。any随着时间的推移,这将鼓励您重新审视有关使用和提高类型安全性的决策。
Consider tracking how well-typed your program is. This will encourage you to revisit decisions about using any and increase type safety over time.
依赖管理在任何语言中都可能令人困惑,TypeScript 也不例外。本章将帮助您建立一个关于依赖项在 TypeScript 中如何工作的心智模型,并向您展示如何解决它们可能带来的一些问题。它还将帮助您制作自己的类型声明文件以发布并与他人共享。通过编写出色的类型声明,您不仅可以帮助您自己的项目,还可以帮助整个 TypeScript 社区。
Dependency management can be confusing in any language, and TypeScript is no exception. This chapter will help you build a mental model for how dependencies work in TypeScript and show you how to work through some of the issues that can come up with them. It will also help you craft your own type declaration files to publish and share with others. By writing great type declarations, you can help not just your own project but the entire TypeScript community.
这节点包管理器 npm 在 JavaScript 世界中无处不在。它提供了一个 JavaScript 库的存储库(npm 注册表)和一种指定您依赖的版本的方法(package.json)。
The Node Package Manager, npm, is ubiquitous in the JavaScript world. It provides both a repository of JavaScript libraries (the npm registry) and a way to specify which versions of them you depend on (package.json).
npm 区分了几种类型的依赖项,每种依赖项都位于package.json的单独部分中:
npm draws a distinction between a few types of dependencies, each of which goes in a separate section of package.json:
dependenciesdependencies这些是运行 JavaScript 所需的包。如果您lodash在运行时导入,那么它应该进入dependencies. 当你在 npm 上发布你的代码并且另一个用户安装它时,它也会安装这些依赖项。(这些被称为传递依赖。)
These are packages that are required to run your JavaScript. If you import lodash at runtime, then it should go in dependencies. When you publish your code on npm and another user installs it, it will also install these dependencies. (These are known as transitive dependencies.)
devDependenciesdevDependencies这些包用于开发和测试您的代码,但在运行时不是必需的。您的测试框架将是devDependency. 与 不同dependencies,这些不会随您的包传递安装。
These packages are used to develop and test your code but are not required at runtime. Your test framework would be an example of a devDependency. Unlike dependencies, these are not installed transitively with your packages.
peerDependenciespeerDependencies这些是您在运行时需要但不想负责跟踪的包。典型的例子是一个插件。你的jQuery 插件与一系列版本的 jQuery 本身兼容,但您更希望用户选择一个版本,而不是您为他们选择。
These are packages that you require at runtime but don’t want to be responsible for tracking. The canonical example is a plug-in. Your jQuery plug-in is compatible with a range of versions of jQuery itself, but you’d prefer that the user select one, rather than you choosing for them.
其中,dependencies和devDependencies是迄今为止最常见的。在使用 TypeScript 时,请注意要添加的依赖项类型。由于 TypeScript 是一种开发工具,并且 TypeScript 类型在运行时不存在(第 3 项),因此与 TypeScript 相关的包一般属于devDependencies.
Of these, dependencies and devDependencies are by far the most common. As you use TypeScript, be aware of which type of dependency you’re adding. Because TypeScript is a development tool and TypeScript types do not exist at runtime (Item 3), packages related to TypeScript generally belong in devDependencies.
首先要考虑的依赖项是 TypeScript 本身。可以在系统范围内安装 TypeScript,但这通常不是一个好主意,原因有二:
The first dependency to consider is TypeScript itself. It is possible to install TypeScript system-wide, but this is generally a bad idea for two reasons:
无法保证您和您的同事将始终安装相同的版本。
There’s no guarantee that you and your coworkers will always have the same version installed.
它为您的项目设置添加了一个步骤。
It adds a step to your project setup.
改用 TypeScript a devDependency。这样,您和您的同事在运行时将始终获得正确的版本npm install。更新您的 TypeScript 版本遵循与更新任何其他包相同的模式。
Make TypeScript a devDependency instead. That way you and your coworkers will always get the correct version when you run npm install. And updating your TypeScript version follows the same pattern as updating any other package.
您的 IDE 和构建工具会很高兴地发现以这种方式安装的 TypeScript 版本。在命令行上,您可以使用npmnpx运行安装的版本:tsc
Your IDE and build tools will happily discover a version of TypeScript installed in this way. On the command line you can use npx to run the version of tsc installed by npm:
$ npx tsc
$ npx tsc
这下一个要考虑的依赖类型是类型依赖或@types. 如果库本身不附带 TypeScript 类型声明,那么您仍然可以在 DefinitelyTyped 上找到类型,DefinitelyTyped 是社区维护的 JavaScript 库类型定义集合。DefinitelyTyped 的类型定义发布在 npm 注册表的范围内@types:@types/jquery具有 jQuery 的类型定义,@types/lodash具有 Lodash 的类型,等等。这些@types包只包含类型。它们不包含实现。
The next type of dependency to consider is type dependencies or @types. If a library itself does not come with TypeScript type declarations, then you may still be able to find typings on DefinitelyTyped, a community-maintained collection of type definitions for JavaScript libraries. Type definitions from DefinitelyTyped are published on the npm registry under the @types scope: @types/jquery has type definitions for the jQuery, @types/lodash has types for Lodash, and so on. These @types packages only contain the types. They don’t contain the implementation.
您的@types依赖项也应该是devDependencies,即使包本身是直接依赖项。例如,要依赖React 及其类型声明,你可以运行:
Your @types dependencies should also be devDependencies, even if the package itself is a direct dependency. For example, to depend on React and its type declarations, you might run:
$ npm 安装反应
$ npm install react
$ npm install --save-dev @types/react
$ npm install --save-dev @types/react
这将生成一个如下所示的package.json文件:
This will result in a package.json file that looks something like this:
{"devDependencies":{"@types/lodash":"^16.8.19","typescript":"^3.5.3"},"dependencies":{"react":"^16.8.6"}}
{"devDependencies":{"@types/lodash":"^16.8.19","typescript":"^3.5.3"},"dependencies":{"react":"^16.8.6"}}
这里的想法是你应该发布 JavaScript,而不是 TypeScript,并且你的 JavaScript 不依赖于@types你运行它的时间。依赖项可能会出现一些问题@types,下一项将更深入地探讨这个主题。
The idea here is that you should publish JavaScript, not TypeScript, and your JavaScript does not depend on the @types when you run it. There are a few things that can go wrong with @types dependencies, and the next item will delve deeper into this topic.
避免在系统范围内安装 TypeScript。让 TypeScript 成为devDependency您项目的一部分,以确保团队中的每个人都使用一致的版本。
Avoid installing TypeScript system-wide. Make TypeScript a devDependency of your project to ensure that everyone on the team is using a consistent version.
将@types依赖项放在 中devDependencies,而不是dependencies. 如果您需要@types在运行时,那么您可能想要重新处理您的流程。
Put @types dependencies in devDependencies, not dependencies. If you need @types at runtime, then you may want to rework your process.
依赖管理很少让软件开发人员产生快乐的感觉。通常你只想使用一个库,而不会过多考虑它的传递依赖项是否与你的兼容。
Dependency management rarely conjures up happy feelings for software developers. Usually you just want to use a library and not think too much about whether its transitive dependencies are compatible with yours.
坏消息是 TypeScript 并没有使它变得更好。事实上,它使依赖管理变得相当复杂。这是因为您现在不必担心一个版本,而是三个:
The bad news is that TypeScript doesn’t make this any better. In fact, it makes dependency management quite a bit more complicated. This is because instead of having a single version to worry about, you now have three:
包的版本
The version of the package
其类型声明的版本 ( @types)
The version of its type declarations (@types)
TypeScript 的版本
The version of TypeScript
如果这些版本中的任何一个版本彼此不同步,您可能会遇到与依赖项管理没有明显关系的错误。但俗话说,“让事情尽可能简单,但不能更简单”。了解 TypeScript 包管理的全部复杂性将有助于您诊断和修复问题。当需要发布您自己的类型声明时,它将帮助您做出更明智的决定。
If any of these versions get out of sync with one another, you can run into errors that may not be clearly related to dependency management. But as the saying goes, “make things as simple as possible but no simpler.” Understanding the full complexity of TypeScript package management will help you diagnose and fix problems. And it will help you make more informed decisions when it comes time to publish type declarations of your own.
以下是 TypeScript 中的依赖项应该如何工作。您将包安装为直接依赖项,并将其类型安装为开发依赖项(请参阅条目 45):
Here’s how dependencies in TypeScript are supposed to work. You install a package as a direct dependency, and you install its types as a dev dependency (see Item 45):
$ npm 安装反应 + 反应@16.8.6 $ npm install --save-dev @types/react + @types/react@16.8.19
$ npm install react + react@16.8.6 $ npm install --save-dev @types/react + @types/react@16.8.19
请注意,主要版本和次要版本 ( 16.8) 匹配,但补丁版本 (.6和.19) 不匹配。这正是您想看到的。版本16.8中的@types表示这些类型声明描述了 版本的16.8API react。假设react模块遵循良好的语义版本控制卫生,补丁版本(16.8.1,,16.8.2...)将不会更改其公共 API,也不需要更新类型声明。但是类型声明本身可能有错误或遗漏。该@types模块的补丁版本对应于这些类型的修复和添加。在这种情况下,类型声明的更新比库本身多得多(19 对 6)。
Note that the major and minor versions (16.8) match but that the patch versions (.6 and .19) do not. This is exactly what you want to see. The 16.8 in the @types version means that these type declarations describe the API of version 16.8 of react. Assuming the react module follows good semantic versioning hygiene, the patch versions (16.8.1, 16.8.2, …) will not change its public API and will not require updates to the type declarations. But the type declarations themselves might have bugs or omissions. The patch versions of the @types module correspond to these sorts of fixes and additions. In this case, there were many more updates to the type declarations than the library itself (19 versus 6).
这可能会在几个方面出错。
This can go wrong in a few ways.
首先,您可能更新了一个库但忘记更新它的类型声明。在这种情况下,每当您尝试使用库的新功能时,您都会遇到类型错误。如果对库进行重大更改,尽管您的代码通过了类型检查器,您仍可能会遇到运行时错误。
First, you might update a library but forget to update its type declarations. In this case you’ll get type errors whenever you try to use new features of the library. If there were breaking changes to the library, you might get runtime errors despite your code passing the type checker.
解决方案通常是更新您的类型声明,以便版本恢复同步。如果类型声明尚未更新,您有几个选择。您可以在自己的项目中使用扩充来添加您想要使用的新函数和方法。或者您可以将更新的类型声明贡献回社区。
The solution is usually to update your type declarations so that the versions are back in sync. If the type declarations have not been updated, you have a few options. You can use an augmentation in your own project to add new functions and methods that you’d like to use. Or you can contribute updated type declarations back to the community.
其次,您的类型声明可能会领先于您的库。如果您一直在使用没有类型的库(也许您使用 给它一个any类型declare module)并稍后尝试安装它们,就会发生这种情况。如果有新版本的库及其类型声明,您的版本可能不同步。其症状与第一个问题类似,只是相反。类型检查器会将您的代码与最新的 API 进行比较,而您将在运行时使用较旧的 API。解决方案是升级库或降级类型声明直到它们匹配。
Second, your type declarations might get ahead of your library. This can happen if you’ve been using a library without its typings (perhaps you gave it an any type using declare module) and try to install them later. If there have been new releases of the library and its type declarations, your versions might be out of sync. The symptoms of this are similar to the first problem, just in reverse. The type checker will be comparing your code against the latest API, while you’ll be using an older one at runtime. The solution is to either upgrade the library or downgrade the type declarations until they match.
第三,类型声明可能需要比您在项目中使用的版本更新的 TypeScript。TypeScript 类型系统的大部分开发都是由尝试更精确地键入流行的 JavaScript 库(如Lodash、React 和 Ramda。这些库的类型声明希望使用最新和最强大的功能来为您提供更好的类型安全性,这是有道理的。
Third, the type declarations might require a newer version of TypeScript than you’re using in your project. Much of the development of TypeScript’s type system has been motivated by an attempt to more precisely type popular JavaScript libraries like Lodash, React, and Ramda. It makes sense that the type declarations for these libraries would want to use the latest and greatest features to get you better type safety.
如果发生这种情况,您将在声明本身中遇到类型错误@types。解决方案是要么升级你的 TypeScript 版本,使用旧版本的类型声明,要么,如果你真的不能更新 TypeScript,用declare module. 库可以通过 为不同版本的 TypeScript 提供不同的类型声明typesVersions,但这种情况很少见:在撰写本文时,只有不到 1% 的包DefinitelyTyped 就是这样做的。
If this happens, you’ll experience it as type errors in the @types declarations themselves. The solution is to either upgrade your TypeScript version, use an older version of the type declarations, or, if you really can’t update TypeScript, stub out the types with declare module. It is possible for a library to provide different type declarations for different versions of TypeScript via typesVersions, but this is rare: at the time of this writing, fewer than 1% of the packages on DefinitelyTyped did so.
要安装@types特定版本的 TypeScript,您可以使用:
To install @types for a specific version of TypeScript, you can use:
npm install --save-dev @types/lodash@ts3.1
npm install --save-dev @types/lodash@ts3.1
库与其类型之间的版本匹配是尽力而为,可能并不总是正确的。但是库越受欢迎,它的类型声明就越有可能做到这一点。
The version matching between libraries and their types is best effort and may not always be correct. But the more popular the library is, the more likely it is that its type declarations will get this right.
第四,您可能会遇到重复的@types依赖项。假设你依赖@types/fooand @types/bar。如果@types/bar依赖于不兼容的版本@types/foo,那么 npm 将尝试通过安装两个版本来解决此问题,其中一个在嵌套文件夹中:
Fourth, you can wind up with duplicate @types dependencies. Say you depend on @types/foo and @types/bar. If @types/bar depends on an incompatible version of @types/foo, then npm will attempt to resolve this by installing both versions, one in a nested folder:
节点模块/
@类型/
富/
索引.d.ts @1.2.3
酒吧/
索引.d.ts
节点模块/
@类型/
富/
索引.d.ts @2.3.4node_modules/
@types/
foo/
index.d.ts @1.2.3
bar/
index.d.ts
node_modules/
@types/
foo/
index.d.ts @2.3.4
虽然这对于在运行时使用的节点模块来说有时是可以的,但对于类型声明来说几乎肯定是不行的,它存在于一个平坦的全局中命名空间。您会看到这是关于重复声明或无法合并的声明的错误。您可以通过运行 来查明为什么有重复的类型声明npm ls @types/foo。解决方案通常是更新您对@types/foo或 的依赖性@types/bar,以便它们兼容。像这样的传递@types依赖通常是麻烦的根源。如果您要发布类型,请参阅第 51 项以了解避免它们的方法。
While this is sometimes OK for node modules that are used at runtime, it almost certainly won’t be OK for type declarations, which live in a flat global namespace. You’ll see this as errors about duplicate declarations or declarations that cannot be merged. You can track down why you have a duplicate type declaration by running npm ls @types/foo. The solution is typically to update your dependency on @types/foo or @types/bar so that they are compatible. Transitive @types dependencies like these are often a source of trouble. If you’re publishing types, see Item 51 for ways to avoid them.
一些包,尤其是那些用 TypeScript 编写的包,选择捆绑它们自己的类型声明。这通常由指向.d.ts文件的package.json"types"中的字段指示:
Some packages, particularly those written in TypeScript, choose to bundle their own type declarations. This is usually indicated by a "types" field in their package.json which points to a .d.ts file:
{"name":"left-pad","version":"1.3.0","description":"String left pad","main":"index.js","types":"index.d.ts",//...}
{"name":"left-pad","version":"1.3.0","description":"String left pad","main":"index.js","types":"index.d.ts",//...}
这是否解决了我们所有的问题?我什至会问答案是否是“是”?
Does this solve all our problems? Would I even be asking if the answer was “yes”?
捆绑类型确实解决了版本不匹配的问题,特别是如果库本身是用 TypeScript 编写的并且类型声明是由tsc. 但是捆绑有其自身的一些问题。
Bundling types does solve the problem of version mismatch, particularly if the library itself is written in TypeScript and the type declarations are generated by tsc. But bundling has some problems of its own.
首先,如果捆绑类型中存在无法通过扩充修复的错误怎么办?或者,这些类型在发布时工作正常,但此后发布了一个新的 TypeScript 版本,它标记了一个错误。你@types可以依赖库的实现而不是它的类型声明。但是对于捆绑类型,您将失去此选项。一个错误的类型声明可能会让你停留在旧版本的 TypeScript 上。将此与 DefinitelyTyped 进行对比:随着 TypeScript 的开发,Microsoft 针对 DefinitelyTyped 上的所有类型声明运行它。休息很快就解决了。
First, what if there’s an error in the bundled types that can’t be fixed through augmentation? Or the types worked fine when they were published, but a new TypeScript version has since been released which flags an error. With @types you could depend on the library’s implementation but not its type declarations. But with bundled types, you lose this option. One bad type declaration might keep you stuck on an old version of TypeScript. Contrast this with DefinitelyTyped: as TypeScript is developed, Microsoft runs it against all the type declarations on DefinitelyTyped. Breaks are fixed quickly.
其次,如果您的类型依赖于另一个库的类型声明怎么办?通常这是一个devDependency(项目 45)。但是如果你发布了你的模块并且另一个用户安装了它,他们将不会得到你的devDependencies. 会导致类型错误。另一方面,您可能也不想让它成为直接依赖项,因为那样您的 JavaScript 用户将@types无缘无故地安装模块。条目 51讨论了针对这种情况的标准解决方法。但是如果你在 DefinitelyTyped 上发布你的类型,这根本不是问题:你在那里声明你的类型依赖,只有你的 TypeScript 用户会得到它。
Second, what if your types depend on another library’s type declarations? Usually this would be a devDependency (Item 45). But if you publish your module and another user installs it, they won’t get your devDependencies. Type errors will result. On the other hand, you probably don’t want to make it a direct dependency either, since then your JavaScript users will install @types modules for no reason. Item 51 discusses the standard workaround for this situation. But if you publish your types on DefinitelyTyped, this is not a problem at all: you declare your type dependency there and only your TypeScript users will get it.
第三,如果您需要解决旧版本库的类型声明问题怎么办?你能回去发布补丁更新吗?DefinitelyTyped 具有同时维护同一库的不同版本的类型声明的机制,这在您自己的项目中可能很难做到。
Third, what if you need to fix an issue with the type declarations of an old version of your library? Would you be able to go back and release a patch update? DefinitelyTyped has mechanisms for simultaneously maintaining type declarations for different versions of the same library, something that might be hard for you to do in your own project.
第四,你对接受类型声明补丁的承诺如何?请记住本项目的版本react以及@types/react从本项目开始的版本。类型声明的补丁更新是库本身的三倍。DefinitelyTyped 由社区维护,能够处理这个数量。特别是,如果库维护者在五天内没有查看补丁,则全球维护者会。您能否为您的图书馆承诺类似的周转时间?
Fourth, how committed to accepting patches for type declarations are you? Remember the versions of react and @types/react from the start of this item. There were three times more patch updates to the type declarations than the library itself. DefinitelyTyped is community-maintained and is able to handle this volume. In particular, if a library maintainer doesn’t look at a patch within five days, a global maintainer will. Can you commit to a similar turnaround time for your library?
在 TypeScript 中管理依赖关系可能具有挑战性,但它确实带来了回报:编写良好的类型声明可以帮助您学习如何正确使用库,并可以大大提高您的工作效率。当您遇到依赖管理问题时,请牢记这三个版本。
Managing dependencies in TypeScript can be challenging, but it does come with rewards: well-written type declarations can help you learn how to use libraries correctly and can greatly improve your productivity with them. As you run into issues with dependency management, keep the three versions in mind.
如果您要发布包,请权衡捆绑类型声明与在 DefinitelyTyped 上发布它们的优缺点。官方建议仅当库是用 TypeScript 编写时才捆绑类型声明。这在实践中效果很好,因为tsc可以自动为您生成类型声明(使用declaration编译器选项)。对于 JavaScript 库,手工制作的类型声明更有可能包含错误,并且它们将需要更多更新。如果您在 DefinitelyTyped 上发布您的类型声明,社区将帮助您支持和维护它们。
If you are publishing packages, weigh the pros and cons of bundling type declarations versus publishing them on DefinitelyTyped. The official recommendation is to bundle type declarations only if the library is written in TypeScript. This works well in practice since tsc can automatically generate type declarations for you (using the declaration compiler option). For JavaScript libraries, handcrafted type declarations are more likely to contain errors, and they’ll require more updates. If you publish your type declarations on DefinitelyTyped, the community will help you support and maintain them.
依赖涉及三个版本@types:库版本、@types版本和 TypeScript 版本。
There are three versions involved in an @types dependency: the library version, the @types version, and the TypeScript version.
如果更新库,请确保更新相应的@types.
If you update a library, make sure you update the corresponding @types.
了解捆绑类型与在 DefinitelyTyped 上发布它们的优缺点。如果您的库是用 TypeScript 编写的,则首选捆绑类型,如果不是,则首选 DefinitelyTyped。
Understand the pros and cons of bundling types versus publishing them on DefinitelyTyped. Prefer bundling types if your library is written in TypeScript and DefinitelyTyped if it is not.
使用TypeScript 足够长,你最终会发现自己想要使用第三方模块的typeor却发现它没有被导出。interface幸运的是,TypeScript 用于类型之间映射的工具足够丰富,作为库用户,您几乎总能找到一种方法来引用您想要的类型。作为图书馆作者,这意味着您应该首先导出您的类型。如果一个类型曾经出现在函数声明中,它就被有效地导出了。所以你不妨把事情说清楚。
Use TypeScript long enough and you’ll eventually find yourself wanting to use a type or interface from a third-party module only to find that it isn’t exported. Fortunately TypeScript’s tools for mapping between types are rich enough that, as a library user, you can almost always find a way to reference the type you want. As a library author, this means that you ought to just export your types to begin with. If a type ever appears in a function declaration, it is effectively exported. So you may as well make things explicit.
假设你想创建一些秘密的、未导出的类型:
Suppose you want to create some secret, unexported types:
interfaceSecretName{first:string;last:string;}interfaceSecretSanta{name:SecretName;gift:string;}exportfunctiongetGift(name:SecretName,gift:string):SecretSanta{// ...}
interfaceSecretName{first:string;last:string;}interfaceSecretSanta{name:SecretName;gift:string;}exportfunctiongetGift(name:SecretName,gift:string):SecretSanta{// ...}
作为您模块的用户,我不能直接导入SecretNameor SecretSanta,只能getGift. 但这不是障碍:因为这些类型出现在导出的函数签名中,所以我可以提取它们。一种方法是使用Parameters和ReturnType通用类型:
As a user of your module, I cannot directly import SecretName or SecretSanta, only getGift. But this is no barrier: because those types appear in an exported function signature, I can extract them. One way is to use the Parameters and ReturnType generic types:
typeMySanta=ReturnType<typeofgetGift>;// SecretSantatypeMyName=Parameters<typeofgetGift>[0];// SecretName
typeMySanta=ReturnType<typeofgetGift>;// SecretSantatypeMyName=Parameters<typeofgetGift>[0];// SecretName
如果您不导出这些类型的目的是为了保持灵活性,那么夹具就完成了!您已经通过将它们放在公共 API 中来承诺它们。帮您的用户一个忙并导出它们。
If your goal in not exporting these types was to preserve flexibility, then the jig is up! You’ve already committed to them by putting them in a public API. Do your users a favor and export them.
导出在任何公共方法中以任何形式出现的类型。您的用户无论如何都可以提取它们,因此您不妨让他们轻松一点。
Export types that appear in any form in any public method. Your users will be able to extract them anyway, so you may as well make it easy for them.
Here’s a TypeScript function to generate a greeting:
// Generate a greeting. Result is formatted for display.functiongreet(name:string,title:string){return`Hello${title}${name}`;}
// Generate a greeting. Result is formatted for display.functiongreet(name:string,title:string){return`Hello${title}${name}`;}
作者非常友好地留下了描述此功能的作用的评论。但对于旨在供函数用户阅读的文档,最好使用 JSDoc 样式的注释:
The author was kind enough to leave a comment describing what this function does. But for documentation intended to be read by users of your functions, it’s better to use JSDoc-style comments:
/** Generate a greeting. Result is formatted for display. */functiongreetJSDoc(name:string,title:string){return`Hello${title}${name}`;}
/** Generate a greeting. Result is formatted for display. */functiongreetJSDoc(name:string,title:string){return`Hello${title}${name}`;}
原因是在调用函数时,编辑器中有一个几乎通用的约定来显示 JSDoc 样式的注释(参见图 6-1)。
The reason is that there is a nearly universal convention in editors to surface JSDoc-style comments when the function is called (see Figure 6-1).
而行内注释没有得到这样的处理(见图6-2)。
Whereas the inline comment gets no such treatment (see Figure 6-2).
这TypeScript 语言服务支持此约定,您应该利用它。如果评论描述了一个公共 API,它应该是 JSDoc。在 TypeScript 的上下文中,这些注释有时称为 TSDoc。您可以使用许多常用约定,例如@paramand @returns:
The TypeScript language service supports this convention, and you should take advantage of it. If a comment describes a public API, it should be JSDoc. In the context of TypeScript, these comments are sometimes called TSDoc. You can use many of the usual conventions like @param and @returns:
/*** Generate a greeting.* @param name Name of the person to greet* @param salutation The person's title* @returns A greeting formatted for human consumption.*/functiongreetFullTSDoc(name:string,title:string){return`Hello${title}${name}`;}
/*** Generate a greeting.* @param name Name of the person to greet* @param salutation The person's title* @returns A greeting formatted for human consumption.*/functiongreetFullTSDoc(name:string,title:string){return`Hello${title}${name}`;}
这让编辑器可以在您编写函数调用时显示每个参数的相关文档(如图6-3所示)。
This lets editors show the relevant documentation for each parameter as you’re writing out a function call (as shown in Figure 6-3).
您还可以将 TSDoc 与类型定义一起使用:
You can also use TSDoc with type definitions:
/** A measurement performed at a time and place. */interfaceMeasurement{/** Where was the measurement made? */position:Vector3D;/** When was the measurement made? In seconds since epoch. */time:number;/** Observed momentum */momentum:Vector3D;}
/** A measurement performed at a time and place. */interfaceMeasurement{/** Where was the measurement made? */position:Vector3D;/** When was the measurement made? In seconds since epoch. */time:number;/** Observed momentum */momentum:Vector3D;}
当您检查Measurement对象中的各个字段时,您将获得上下文文档(参见图 6-4)。
As you inspect individual fields in a Measurement object, you’ll get contextual documentation (see Figure 6-4).
TSDoc 注释使用 Markdown 格式化,所以如果你想使用粗体、斜体或项目符号列表,你可以(见图6-5):
TSDoc comments are formatted using Markdown, so if you want to use bold, italic, or bulleted lists, you can (see Figure 6-5):
/*** This _interface_ has **three** properties:* 1. x* 2. y* 3. z*/interfaceVector3D{x:number;y:number;z:number;}
/*** This _interface_ has **three** properties:* 1. x* 2. y* 3. z*/interfaceVector3D{x:number;y:number;z:number;}
不过,请尽量避免在您的文档中写文章:最好的评论是简短而切中要点。
Try to avoid writing essays in your documentation, though: the best comments are short and to the point.
JSDoc 包含一些用于指定类型信息的约定 ( @param {string} name ...),但你应该避免这些约定以支持 TypeScript 类型 ( Item 30 )。
JSDoc includes some conventions for specifying type information (@param {string} name ...), but you should avoid these in favor of TypeScript types (Item 30).
使用 JSDoc-/TSDoc 格式的注释来记录导出的函数、类和类型。这有助于编辑在最相关的时候为您的用户呈现信息。
Use JSDoc-/TSDoc-formatted comments to document exported functions, classes, and types. This helps editors surface information for your users when it’s most relevant.
使用@param、@returns和 Markdown 进行格式化。
Use @param, @returns, and Markdown for formatting.
避免在文档中包含类型信息(参见条目 30)。
Avoid including type information in documentation (see Item 30).
JavaScript的 this关键字是该语言中最令人困惑的部分之一。与使用letor声明的变量不同const,它们是词法范围的,是动态范围的:它的值不取决于定义this它的方式,而是取决于它被调用的方式。
JavaScript’s this keyword is one of the most notoriously confusing parts of the language. Unlike variables declared with let or const, which are lexically scoped, this is dynamically scoped: its value depends not on the way in which it was defined but on the way in which it was called.
this最常用于类中,通常引用对象的当前实例:
this is most often used in classes, where it typically references the current instance of an object:
classC{vals=[1,2,3];logSquares() {for(constvalofthis.vals){console.log(val*val);}}}constc=newC();c.logSquares();
classC{vals=[1,2,3];logSquares() {for(constvalofthis.vals){console.log(val*val);}}}constc=newC();c.logSquares();
这记录:
This logs:
1个 4个 9
1 4 9
现在看看如果你尝试放入logSquares一个变量并调用它会发生什么:
Now look what happens if you try to put logSquares in a variable and call that:
constc=newC();constmethod=c.logSquares;method();
constc=newC();constmethod=c.logSquares;method();
此版本在运行时抛出错误:
This version throws an error at runtime:
未捕获的类型错误:无法读取未定义的属性“vals”
Uncaught TypeError: Cannot read property 'vals' of undefined
问题在于它c.logSquares()实际上做了两件事:调用C.prototype.logSquares 并将this该函数中的值绑定到c. 通过提取对 的引用logSquares,您将它们分开,并this设置为undefined。
The problem is that c.logSquares() actually does two things: it calls C.prototype.logSquares and it binds the value of this in that function to c. By pulling out a reference to logSquares, you’ve separated these, and this gets set to undefined.
JavaScript 让您可以完全控制this绑定。您可以使用call明确设置this和修复问题:
JavaScript gives you complete control over this binding. You can use call to explicitly set this and fix the problem:
constc=newC();constmethod=c.logSquares;method.call(c);// Logs the squares again
constc=newC();constmethod=c.logSquares;method.call(c);// Logs the squares again
没有理由必须this绑定到C. 它可以绑定到任何东西。this因此,图书馆可以而且确实在发挥其部分 API的价值。甚至 DOM 也利用了这一点。在事件处理程序中,例如:
There’s no reason that this had to be bound to an instance of C. It could have been bound to anything. So libraries can, and do, make the value of this part of their APIs. Even the DOM makes use of this. In an event handler, for instance:
document.querySelector('input')!.addEventListener('change',function(e){console.log(this);// Logs the input element on which the event fired.});
document.querySelector('input')!.addEventListener('change',function(e){console.log(this);// Logs the input element on which the event fired.});
this绑定经常出现在像这样的回调的上下文中。onClick例如,如果你想在一个类中定义一个处理程序,你可以试试这个:
this binding often comes up in the context of callbacks like this one. If you want to define an onClick handler in a class, for example, you might try this:
classResetButton{render() {returnmakeButton({text:'Reset',onClick:this.onClick});}onClick() {alert(`Reset${this}`);}}
classResetButton{render() {returnmakeButton({text:'Reset',onClick:this.onClick});}onClick() {alert(`Reset${this}`);}}
Button调用时onClick,它会提示“Reset undefined”。哎呀!像往常一样,罪魁祸首是this有约束力的。一个常见的解决方案是在构造函数中创建方法的绑定版本:
When Button calls onClick, it will alert “Reset undefined.” Oops! As usual, the culprit is this binding. A common solution is to create a bound version of the method in the constructor:
classResetButton{constructor(){this.onClick=this.onClick.bind(this);}render() {returnmakeButton({text:'Reset',onClick:this.onClick});}onClick() {alert(`Reset${this}`);}}
classResetButton{constructor(){this.onClick=this.onClick.bind(this);}render() {returnmakeButton({text:'Reset',onClick:this.onClick});}onClick() {alert(`Reset${this}`);}}
该onClick() { ... }定义在 上定义了一个属性ResetButton.prototype。这由 的所有实例共享ResetButton。this.onClick = ...当您在构造函数中绑定时,它会创建一个在绑定到该实例onClick的实例上调用的属性。实例属性在查找序列中位于原型属性之前,因此引用方法中的绑定函数。ResetButtonthisonClickonClickthis.onClickrender()
The onClick() { ... } definition defines a property on ResetButton.prototype. This is shared by all instances of ResetButton. When you bind this.onClick = ... in the constructor, it creates a property called onClick on the instance of ResetButton with this bound to that instance. The onClick instance property comes before the onClick prototype property in the lookup sequence, so this.onClick refers to the bound function in the render() method.
绑定的简写有时很方便:
There is a shorthand for binding that can sometimes be convenient:
classResetButton{render() {returnmakeButton({text:'Reset',onClick:this.onClick});}onClick=()=>{alert(`Reset${this}`);// "this" always refers to the ResetButton instance.}}
classResetButton{render() {returnmakeButton({text:'Reset',onClick:this.onClick});}onClick=()=>{alert(`Reset${this}`);// "this" always refers to the ResetButton instance.}}
这里我们onClick用箭头函数代替了。这将在每次ResetButton构造 a 并this设置为适当的值时定义一个新函数。看看这生成的 JavaScript 是有启发性的:
Here we’ve replaced onClick with an arrow function. This will define a new function every time a ResetButton is constructed with this set to the appropriate value. It’s instructive to look at the JavaScript that this generates:
classResetButton{constructor(){var_this=this;this.onClick=function(){alert("Reset "+_this);};}render(){returnmakeButton({text:'Reset',onClick:this.onClick});}}
classResetButton{constructor(){var_this=this;this.onClick=function(){alert("Reset "+_this);};}render(){returnmakeButton({text:'Reset',onClick:this.onClick});}}
那么这一切与 TypeScript 有什么关系呢?因为this绑定是 JavaScript 的一部分,所以 TypeScript 对其建模。这意味着如果您正在编写(或键入)一个设置this回调值的库,那么您也应该对此建模。
So what does this all have to do with TypeScript? Because this binding is part of JavaScript, TypeScript models it. This means that if you’re writing (or typing) a library that sets the value of this on callbacks, then you should model this, too.
this您可以通过向回调中添加一个参数来执行此操作:
You do this by adding a this parameter to your callback:
functionaddKeyListener(el:HTMLElement,fn:(this:HTMLElement,e:KeyboardEvent)=>void){el.addEventListener('keydown',e=>{fn.call(el,e);});}
functionaddKeyListener(el:HTMLElement,fn:(this:HTMLElement,e:KeyboardEvent)=>void){el.addEventListener('keydown',e=>{fn.call(el,e);});}
该this参数很特殊:它不仅仅是另一个位置参数。如果您尝试使用两个参数调用它,您会看到这一点:
The this parameter is special: it’s not just another positional argument. You can see this if you try to call it with two parameters:
functionaddKeyListener(el:HTMLElement,fn:(this:HTMLElement,e:KeyboardEvent)=>void){el.addEventListener('keydown',e=>{fn(el,e);// ~ Expected 1 arguments, but got 2});}
functionaddKeyListener(el:HTMLElement,fn:(this:HTMLElement,e:KeyboardEvent)=>void){el.addEventListener('keydown',e=>{fn(el,e);// ~ Expected 1 arguments, but got 2});}
更好的是,TypeScript 将强制您使用正确的上下文调用该函数this:
Even better, TypeScript will enforce that you call the function with the correct this context:
functionaddKeyListener(el:HTMLElement,fn:(this:HTMLElement,e:KeyboardEvent)=>void){el.addEventListener('keydown',e=>{fn(e);// ~~~~~ The 'this' context of type 'void' is not assignable// to method's 'this' of type 'HTMLElement'});}
functionaddKeyListener(el:HTMLElement,fn:(this:HTMLElement,e:KeyboardEvent)=>void){el.addEventListener('keydown',e=>{fn(e);// ~~~~~ The 'this' context of type 'void' is not assignable// to method's 'this' of type 'HTMLElement'});}
作为此函数的用户,您可以this在回调中引用并获得完整的类型安全性:
As a user of this function, you can reference this in the callback and get full type safety:
declareletel:HTMLElement;addKeyListener(el,function(e){this.innerHTML;// OK, "this" has type of HTMLElement});
declareletel:HTMLElement;addKeyListener(el,function(e){this.innerHTML;// OK, "this" has type of HTMLElement});
当然,如果你在这里使用箭头函数,你将覆盖this. TypeScript 会捕捉到这个问题:
Of course, if you use an arrow function here, you’ll override the value of this. TypeScript will catch the issue:
classFoo{registerHandler(el:HTMLElement){addKeyListener(el,e=>{this.innerHTML;// ~~~~~~~~~ Property 'innerHTML' does not exist on type 'Foo'});}}
classFoo{registerHandler(el:HTMLElement){addKeyListener(el,e=>{this.innerHTML;// ~~~~~~~~~ Property 'innerHTML' does not exist on type 'Foo'});}}
别忘了this!this如果您在回调中设置 的值,那么它就是您的 API 的一部分,您应该将其包含在您的类型声明中。
Don’t forget about this! If you set the value of this in your callbacks, then it’s part of your API, and you should include it in your type declarations.
了解this绑定的工作原理。
Understand how this binding works.
this当它是 API 的一部分时,为回调提供一个类型。
Provide a type for this in callbacks when it’s part of your API.
如何你会为这个 JavaScript 函数写一个类型声明吗?
How would you write a type declaration for this JavaScript function?
functiondouble(x){returnx+x;}
functiondouble(x){returnx+x;}
double可以传递 astring或 a number。所以你可以使用联合类型:
double can be passed either a string or a number. So you might use a union type:
functiondouble(x:number|string):number|string;functiondouble(x:any){returnx+x;}
functiondouble(x:number|string):number|string;functiondouble(x:any){returnx+x;}
(这些示例都使用了 TypeScript 的函数重载概念。要回顾一下,请参阅第 3 项。)
(These examples all make use of TypeScript’s concept of function overloading. For a refresher, see Item 3.)
虽然这个声明是准确的,但它有点不精确:
While this declaration is accurate, it’s a bit imprecise:
constnum=double(12);// string | numberconststr=double('x');// string | number
constnum=double(12);// string | numberconststr=double('x');// string | number
当double传递 a时number,它返回 a number。当它通过 a 时string,它返回 a string。这个声明忽略了细微差别,并且会产生难以使用的类型。
When double is passed a number, it returns a number. And when it’s passed a string, it returns a string. This declaration misses that nuance and will produce types that are hard to work with.
您可以尝试使用泛型来捕捉这种关系:
You might try to capture this relationship using a generic:
functiondouble<Textendsnumber|string>(x:T):T;functiondouble(x:any){returnx+x;}constnum=double(12);// Type is 12conststr=double('x');// Type is "x"
functiondouble<Textendsnumber|string>(x:T):T;functiondouble(x:any){returnx+x;}constnum=double(12);// Type is 12conststr=double('x');// Type is "x"
不幸的是,在我们对精确度的热情中,我们已经过头了。这些类型现在有点太精确了。当传递一个string类型时,这个double声明将产生一个string类型,这是正确的。但是当传递一个字符串文字类型时,返回类型是相同的字符串文字类型。这是错误的:加倍'x'结果是'xx',而不是'x'。
Unfortunately, in our zeal for precision we’ve overshot. The types are now a little too precise. When passed a string type, this double declaration will result in a string type, which is correct. But when passed a string literal type, the return type is the same string literal type. This is wrong: doubling 'x' results in 'xx', not 'x'.
另一种选择是提供多个类型声明。虽然 TypeScript 只允许你编写一个函数的一个实现,但它允许你编写任意数量的类型声明。您可以使用它来改进以下类型double:
Another option is to provide multiple type declarations. While TypeScript only allows you to write one implementation of a function, it allows you to write any number of type declarations. You can use this to improve the type of double:
functiondouble(x:number):number;functiondouble(x:string):string;functiondouble(x:any){returnx+x;}constnum=double(12);// Type is numberconststr=double('x');// Type is string
functiondouble(x:number):number;functiondouble(x:string):string;functiondouble(x:any){returnx+x;}constnum=double(12);// Type is numberconststr=double('x');// Type is string
这是进步!但这个声明正确吗?不幸的是,仍然存在一个微妙的错误。此类型声明将适用于 astring或 a的值number,但不适用于可能是以下任一值的值:
This is progress! But is this declaration correct? Unfortunately there’s still a subtle bug. This type declaration will work with values that are either a string or a number, but not with values that could be either:
functionf(x:number|string){returndouble(x);// ~ Argument of type 'string | number' is not assignable// to parameter of type 'string'}
functionf(x:number|string){returndouble(x);// ~ Argument of type 'string | number' is not assignable// to parameter of type 'string'}
此调用double是安全的,应该会返回string|number。当您重载类型声明时,TypeScript 会一个一个地处理它们,直到找到匹配项。您看到的错误是上次重载(版本)失败的结果string,因为string|number无法分配给string.
This call to double is safe and should return string|number. When you overload type declarations, TypeScript processes them one by one until it finds a match. The error you’re seeing is a result of the last overload (the string version) failing, because string|number is not assignable to string.
虽然您可以通过添加第三个string|number重载来解决此问题,但最好的解决方案是使用条件类型。条件类型就像类型空间中的 if 语句(条件语句)。它们非常适合像这种情况,您需要涵盖以下几种可能性:
While you could patch this issue by adding a third string|number overload, the best solution is to use a conditional type. Conditional types are like if statements (conditionals) in type space. They’re perfect for situations like this one where there are a few possibilities that you need to cover:
functiondouble<Textendsnumber|string>(x:T):Textendsstring?string:number;functiondouble(x:any){returnx+x;}
functiondouble<Textendsnumber|string>(x:T):Textendsstring?string:number;functiondouble(x:any){returnx+x;}
这类似于第一次尝试double使用泛型进行类型化,但返回类型更为复杂。阅读条件类型就像阅读?:JavaScript 中的三元 () 运算符一样:
This is similar to the first attempt to type double using a generic, but with a more elaborate return type. You read the conditional type like you’d read a ternary (?:) operator in JavaScript:
如果T是string(例如,string字符串文字或字符串文字的并集)的子集,则返回类型为string.
If T is a subset of string (e.g., string or a string literal or a union of string literals), then the return type is string.
否则返回number。
Otherwise return number.
有了这个声明,我们所有的例子都可以工作:
With this declaration, all of our examples work:
constnum=double(12);// numberconststr=double('x');// string// function f(x: string | number): string | numberfunctionf(x:number|string){returndouble(x);}
constnum=double(12);// numberconststr=double('x');// string// function f(x: string | number): string | numberfunctionf(x:number|string){returndouble(x);}
该number|string示例之所以有效,是因为条件类型分布在联合上。当T是 时number|string,TypeScript 解析条件类型如下:
The number|string example works because conditional types distribute over unions. When T is number|string, TypeScript resolves the conditional type as follows:
(数字|字符串) 扩展字符串 ? 字符串:数字 ->(数字扩展字符串?字符串:数字)| (字符串扩展字符串?字符串:数字) -> 号码 | 细绳
(number|string) extends string ? string : number -> (number extends string ? string : number) | (string extends string ? string : number) -> number | string
虽然使用重载的类型声明编写起来更简单,但使用条件类型的版本更正确,因为它泛化到个别情况的并集。这通常是过载的情况。尽管重载是独立处理的,但类型检查器可以将条件类型分析为单个表达式,将它们分布在联合上。如果您发现自己正在编写重载类型声明,请考虑使用条件类型是否可以更好地表达它。
While the type declaration using overloading was simpler to write, the version using conditional types is more correct because it generalizes to the union of the individual cases. This is often the case for overloads. Whereas overloads are treated independently, the type checker can analyze conditional types as a single expression, distributing them over unions. If you find yourself writing an overloaded type declarations, consider whether it might be better expressed using a conditional type.
优先使用条件类型而不是重载类型声明。通过分布在联合上,条件类型允许您的声明支持联合类型而无需额外的重载。
Prefer conditional types to overloaded type declarations. By distributing over unions, conditional types allow your declarations to support union types without additional overloads.
认为你已经编写了一个用于解析的库CSV 文件。它的 API 很简单:您传入 CSV 文件的内容并取回将列名映射到值的对象列表。为了方便您的 NodeJS 用户,您允许内容是 astring或 NodeJS Buffer:
Suppose you’ve written a library for parsing CSV files. Its API is simple: you pass in the contents of the CSV file and get back a list of objects mapping column names to values. As a convenience for your NodeJS users, you allow the contents to be either a string or a NodeJS Buffer:
functionparseCSV(contents:string|Buffer):{[column:string]:string}[]{if(typeofcontents==='object'){// It's a bufferreturnparseCSV(contents.toString('utf8'));}// ...}
functionparseCSV(contents:string|Buffer):{[column:string]:string}[]{if(typeofcontents==='object'){// It's a bufferreturnparseCSV(contents.toString('utf8'));}// ...}
的类型定义Buffer来自您必须安装的 NodeJS 类型声明:
The type definition for Buffer comes from the NodeJS type declarations, which you must install:
npm install --save-dev @types/node
npm install --save-dev @types/node
当您发布 CSV 解析库时,您会在其中包含类型声明。由于您的类型声明依赖于 NodeJS 类型,因此您将它们作为devDependency(Item 45)包括在内。如果这样做,您可能会收到两组用户的投诉:
When you publish your CSV parsing library, you include the type declarations with it. Since your type declarations depend on the NodeJS types, you include these as a devDependency (Item 45). If you do this, you’re liable to get complaints from two groups of users:
@types想知道这些模块所依赖的是什么的 JavaScript 开发人员。
JavaScript developers who wonder what these @types modules are that they’re depending on.
想知道为什么要依赖 NodeJS 的 TypeScript Web 开发人员。
TypeScript web developers who wonder why they’re depending on NodeJS.
这些抱怨是合理的。该Buffer行为不是必需的,仅与已经使用 NodeJS 的用户相关。并且中的声明@types/node仅与同样使用 TypeScript 的 NodeJS 用户相关。
These complaints are reasonable. The Buffer behavior isn’t essential and is only relevant for users who are using NodeJS already. And the declaration in @types/node is only relevant to NodeJS users who are also using TypeScript.
TypeScript 的结构类型(第 4 项)可以帮助您摆脱困境。您可以只使用所需的方法和属性编写自己的声明,而不是使用Bufferfrom的声明。@types/node在这种情况下,这只是一个toString接受编码的方法:
TypeScript’s structural typing (Item 4) can help you out of the jam. Rather than using the declaration of Buffer from @types/node, you can write your own with just the methods and properties you need. In this case that’s just a toString method that accepts an encoding:
interfaceCsvBuffer{toString(encoding:string):string;}functionparseCSV(contents:string|CsvBuffer):{[column:string]:string}[]{// ...}
interfaceCsvBuffer{toString(encoding:string):string;}functionparseCSV(contents:string|CsvBuffer):{[column:string]:string}[]{// ...}
这个界面比完整的界面短得多,但它确实从Buffer. 在 NodeJS 项目中,parseCSV使用 real调用Buffer仍然可以,因为类型是兼容的:
This interface is dramatically shorter than the complete one, but it does capture our (simple) needs from a Buffer. In a NodeJS project, calling parseCSV with a real Buffer is still OK because the types are compatible:
parseCSV(newBuffer("column1,column2\nval1,val2","utf-8"));// OK
parseCSV(newBuffer("column1,column2\nval1,val2","utf-8"));// OK
如果您的库只依赖于另一个库的类型,而不是它的实现,请考虑将您需要的声明镜像到您自己的代码中。这将为您的 TypeScript 用户带来类似的体验,并为其他所有人带来更好的体验。
If your library only depends on the types for another library, rather than its implementation, consider mirroring just the declarations you need into your own code. This will result in a similar experience for your TypeScript users and an improved experience for everyone else.
如果您依赖库的实现,您仍然可以应用相同的技巧来避免依赖于它的类型。但随着依赖性越来越大、越来越重要,这变得越来越困难。如果您正在为另一个库复制大部分类型声明,您可能希望通过使@types依赖关系显式化来形式化关系。
If you depend on the implementation of a library, you may still be able to apply the same trick to avoid depending on its typings. But this becomes increasingly difficult as the dependence grows larger and more essential. If you’re copying a large portion of the type declarations for another library, you may want to formalize the relationship by making the @types dependency explicit.
此技术还有助于切断单元测试和生产系统之间的依赖关系。请参阅第 4 项getAuthors中的示例。
This technique is also helpful for severing dependencies between your unit tests and production systems. See the getAuthors example in Item 4.
使用结构类型来切断不重要的依赖关系。
Use structural typing to sever dependencies that are nonessential.
不要强迫 JavaScript 用户依赖@types. 不要强迫 Web 开发人员依赖 NodeJS。
Don’t force JavaScript users to depend on @types. Don’t force web developers to depend on NodeJS.
你不会在没有为它编写测试的情况下发布代码(我希望!),你也不应该在没有为它们编写测试的情况下发布类型声明。但是你如何测试类型呢?如果您正在编写类型声明,那么测试是一项必不可少但令人惊讶的艰巨任务。使用 TypeScript 提供的工具对类型系统内部的类型进行断言是很诱人的。但是这种方法有几个缺陷。最终它更安全,更直接使用dtslint或从类型系统外部检查类型的类似工具。
You wouldn’t publish code without writing tests for it (I hope!), and you shouldn’t publish type declarations without writing tests for them, either. But how do you test types? If you’re authoring type declarations, testing is an essential but surprisingly fraught undertaking. It’s tempting to make assertions about types inside the type system using the tools that TypeScript provides. But there are several pitfalls with this approach. Ultimately it’s safer and more straightforward to use dtslint or a similar tool that inspects types from outside of the type system.
假设您已经map为实用程序库(流行的Lodash 和 Underscore 库都提供了这样的功能):
Suppose you’ve written a type declaration for a map function provided by a utility library (the popular Lodash and Underscore libraries both provide such a function):
declarefunctionmap<U,V>(array:U[],fn:(u:U)=>V):V[];
declarefunctionmap<U,V>(array:U[],fn:(u:U)=>V):V[];
您如何检查此类型声明是否产生预期的类型?(大概有单独的实现测试。)一种常见的技术是编写一个调用函数的测试文件:
How can you check that this type declaration results in the expected types? (Presumably there are separate tests for the implementation.) One common technique is to write a test file that calls the function:
map(['2017','2018','2019'],v=>Number(v));
map(['2017','2018','2019'],v=>Number(v));
这将做一些直截了当的错误检查:如果你的声明map只列出了一个参数,这会发现错误。但是是不是感觉这里少了点什么?
This will do some blunt error checking: if your declaration of map only listed a single parameter, this would catch the mistake. But does it feel like something is missing here?
这种运行时行为测试风格的等价物可能如下所示:
The equivalent of this style of test for runtime behavior might look something like this:
test('square a number',()=>{square(1);square(2);});
test('square a number',()=>{square(1);square(2);});
当然,这会测试该square函数不会抛出错误。但它缺少对返回值的任何检查,因此没有真正的行为测试。的不正确实现square仍会通过此测试。
Sure, this tests that the square function doesn’t throw an error. But it’s missing any checks on the return value, so there’s no real test of the behavior. An incorrect implementation of square would still pass this test.
这种方法在测试类型声明文件中很常见,因为它很容易复制库的现有单元测试。虽然它确实提供了一些价值,但实际检查某些类型会更好!
This approach is common in testing type declaration files because it’s simple to copy over existing unit tests for a library. And while it does provide some value, it would be much better to actually check some types!
一种方法是将结果分配给具有特定类型的变量:
One way is to assign the result to a variable with a specific type:
constlengths:number[]=map(['john','paul'],name=>name.length);
constlengths:number[]=map(['john','paul'],name=>name.length);
这正是条款 19鼓励您删除的那种多余的类型声明。但在这里它起着至关重要的作用:它提供了一些信心,即map声明至少对类型做了一些合理的事情。事实上,您可以在 DefinitelyTyped 中找到许多使用这种方法进行测试的类型声明。但是,正如我们将看到的,使用赋值进行测试存在一些基本问题。
This is exactly the sort of superfluous type declaration that Item 19 would encourage you to remove. But here it plays an essential role: it provides some confidence that the map declaration is at least doing something sensible with the types. And indeed you can find many type declarations in DefinitelyTyped that use exactly this approach for testing. But, as we’ll see, there are a few fundamental problems with using assignment for testing.
一是您必须创建一个可能未使用的命名变量。这增加了样板文件,但也意味着您必须禁用某些形式的 linting。
One is that you have to create a named variable that is likely to be unused. This adds boilerplate, but also means that you’ll have to disable some forms of linting.
一个常见的解决方法是定义一个助手:
A common workaround is to define a helper:
functionassertType<T>(x:T){}assertType<number[]>(map(['john','paul'],name=>name.length));
functionassertType<T>(x:T){}assertType<number[]>(map(['john','paul'],name=>name.length));
这消除了未使用的变量问题,但仍然有惊喜。
This eliminates the unused variable issue, but there are still surprises.
第二个问题是我们正在检查两种类型的可分配性而不是相等性。通常这会如您所愿地工作。例如:
A second issue is that we’re checking assignability of the two types rather than equality. Often this works as you’d expect. For example:
constn=12;assertType<number>(n);// OK
constn=12;assertType<number>(n);// OK
如果你检查这个n符号,你会发现它的类型实际上是12,一个数字文字类型。这是 的子类型number,因此可分配性检查通过了,正如您所期望的那样。
If you inspect the n symbol, you’ll see that its type is actually 12, a numeric literal type. This is a subtype of number and so the assignability check passes, just as you’d expect.
到目前为止,一切都很好。但是当你开始检查对象的类型时,事情就变得更加模糊了:
So far so good. But things get murkier when you start checking the types of objects:
constbeatles=['john','paul','george','ringo'];assertType<{name:string}[]>(map(beatles,name=>({name,inYellowSubmarine:name==='ringo'})));// OK
constbeatles=['john','paul','george','ringo'];assertType<{name:string}[]>(map(beatles,name=>({name,inYellowSubmarine:name==='ringo'})));// OK
该map调用返回一个对象数组{name: string, inYellowSubmarine: boolean}。这当然可以分配给{name: string}[],但是我们不应该被迫承认黄色潜水艇吗?根据上下文,您可能真的想也可能不想检查类型是否相等。
The map call returns an array of {name: string, inYellowSubmarine: boolean} objects. This is assignable to {name: string}[], sure, but shouldn’t we be forced to acknowledge the yellow submarine? Depending on the context you may or may not really want to check for type equality.
如果您的函数返回另一个函数,您可能会对什么被认为是可分配的感到惊讶:
If your function returns another function, you may be surprised at what’s considered assignable:
constadd=(a::::number,b_number)=>a+b;assertType<(a_number,b_number)=>number>(add);// OKconstdouble=(x:number)=>2*x;assertType<(a:number,b:number)=>number>(double);// OK!?
constadd=(a:number,b:number)=>a+b;assertType<(a:number,b:number)=>number>(add);// OKconstdouble=(x:number)=>2*x;assertType<(a:number,b:number)=>number>(double);// OK!?
您对第二个断言成功感到惊讶吗?原因是 TypeScript 中的函数可分配给函数类型,该函数类型采用较少的参数:
Are you surprised that the second assertion succeeds? The reason is that a function in TypeScript is assignable to a function type, which takes fewer parameters:
constg:(x:string)=>any=()=>12;// OK
constg:(x:string)=>any=()=>12;// OK
这反映了这样一个事实,即使用比声明的参数更多的参数调用 JavaScript 函数是完全可以的。TypeScript 选择对这种行为进行建模而不是禁止它,主要是因为它在回调中很普遍。中的回调map例如,Lodash函数最多需要三个参数:
This reflects the fact that it’s perfectly fine to call a JavaScript function with more parameters than it’s declared to take. TypeScript chooses to model this behavior rather than bar it, largely because it is pervasive in callbacks. The callback in the Lodash map function, for example, takes up to three parameters:
map(array,(name,index,array)=>{/* ... */});
map(array,(name,index,array)=>{/* ... */});
虽然所有三个都可用,但仅使用一个或有时两个是很常见的,正如我们目前在该项目中所做的那样。事实上,同时使用这三种方法的情况非常少见。通过禁止此分配,TypeScript 将报告大量 JavaScript 代码中的错误。
While all three are available, it’s very common to use only one or sometimes two, as we have so far in this item. In fact, it’s quite rare to use all three. By disallowing this assignment, TypeScript would report errors in an enormous amount of JavaScript code.
所以,你可以做什么?您可以分解函数类型并使用泛型Parameters和ReturnType类型测试其部分:
So what can you do? You could break apart the function type and test its pieces using the generic Parameters and ReturnType types:
constdouble=(x:number)=>2*x;letp:Parameters<typeofdouble>=null!;assertType<[number,number]>(p);// ~ Argument of type '[number]' is not// assignable to parameter of type [number, number]letr:ReturnType<typeofdouble>=null!;assertType<number>(r);// OK
constdouble=(x:number)=>2*x;letp:Parameters<typeofdouble>=null!;assertType<[number,number]>(p);// ~ Argument of type '[number]' is not// assignable to parameter of type [number, number]letr:ReturnType<typeofdouble>=null!;assertType<number>(r);// OK
但是,如果“this”还不够复杂,还有另一个问题:为其回调map设置值。thisTypeScript 可以对这种行为进行建模(参见条款 49),因此您的类型声明应该这样做。你应该测试它。我们该怎么做?
But if “this” isn’t complicated enough, there’s another issue: map sets the value of this for its callback. TypeScript can model this behavior (see Item 49), so your type declaration should do so. And you should test it. How can we do that?
到目前为止,我们的测试map在风格上有点黑盒:我们运行了一个数组和函数map并测试了结果的类型,但我们还没有测试中间步骤的细节。我们可以通过填写回调函数并验证其参数的类型来实现,this直接:
Our tests of map so far have been a bit black box in style: we’ve run an array and function through map and tested the type of the result, but we haven’t tested the details of the intermediate steps. We can do so by filling out the callback function and verifying the types of its parameters and this directly:
constbeatles=['john','paul','george','ringo'];assertType<number[]>(map(beatles,function(name,i,array){// ~~~~~~~ Argument of type '(name: any, i: any, array: any) => any' is// not assignable to parameter of type '(u: string) => any'assertType<string>(name);assertType<number>(i);assertType<string[]>(array);assertType<string[]>(this);// ~~~~ 'this' implicitly has type 'any'returnname.length;}));
constbeatles=['john','paul','george','ringo'];assertType<number[]>(map(beatles,function(name,i,array){// ~~~~~~~ Argument of type '(name: any, i: any, array: any) => any' is// not assignable to parameter of type '(u: string) => any'assertType<string>(name);assertType<number>(i);assertType<string[]>(array);assertType<string[]>(this);// ~~~~ 'this' implicitly has type 'any'returnname.length;}));
这使我们的 声明出现了一些问题map。请注意非箭头函数的使用,以便我们可以测试this.
This surfaced a few issues with our declaration of map. Note the use of a non-arrow function so that we could test the type of this.
这是通过检查的声明:
Here is a declaration that passes the checks:
declarefunctionmap<U,V>(array::::U[],fn:(this:U[],u_U,i_number,array_U[])=>V):V[];
declarefunctionmap<U,V>(array:U[],fn:(this:U[],u:U,i:number,array:U[])=>V):V[];
然而,还有最后一个问题,这是一个主要问题。这是我们模块的完整类型声明文件,它甚至可以通过最严格的测试map,但比无用还糟糕:
There remains a final issue, however, and it is a major one. Here’s a complete type declaration file for our module that will pass even the most stringent tests for map but is worse than useless:
declaremodule'overbar';
declaremodule'overbar';
这any为整个模块分配了一个类型。你的测试都会通过,但你不会有任何类型安全。更糟糕的是,每次调用这个模块中的函数都会悄悄地产生一个any类型,从而传染性地破坏整个代码中的类型安全。即使使用noImplicitAny,您仍然可以any通过类型声明获取类型。
This assigns an any type to the entire module. Your tests will all pass, but you won’t have any type safety. What’s worse, every call to a function in this module will quietly produce an any type, contagiously destroying type safety throughout your code. Even with noImplicitAny, you can still get any types through type declarations.
除非使用一些高级技巧,否则很难any从类型系统中检测到类型。这就是为什么测试类型声明的首选方法是使用在类型检查器之外运行的工具。
Barring some advanced trickery, it’s quite difficult to detect an any type from within the type system. This is why the preferred method for testing type declarations is to use a tool that operates outside the type checker.
为了DefinitelyTyped 存储库中的类型声明,此工具是dtslint. 它通过特殊格式的注释进行操作。map以下是您可以如何使用为函数编写最后一个测试dtslint:
For type declarations in the DefinitelyTyped repository, this tool is dtslint. It operates through specially formatted comments. Here’s how you might write the last test for the map function using dtslint:
constbeatles=['john','paul','george','ringo'];map(beatles,function(name,// $ExpectType stringi,// $ExpectType numberarray// $ExpectType string[]){this// $ExpectType string[]returnname.length;});// $ExpectType number[]
constbeatles=['john','paul','george','ringo'];map(beatles,function(name,// $ExpectType stringi,// $ExpectType numberarray// $ExpectType string[]){this// $ExpectType string[]returnname.length;});// $ExpectType number[]
不是检查可分配性,而是dtslint检查每个符号的类型并进行文本比较。这与您在编辑器中手动测试类型声明的方式相匹配:dtslint本质上是自动执行此过程。这种方法确实有一些缺点:number|string并且string|number在文本上不同但类型相同。string但是and也是如此any,尽管可以相互分配,这才是真正的重点。
Rather than checking assignability, dtslint inspects the type of each symbol and does a textual comparison. This matches how you’d manually test the type declarations in your editor: dtslint essentially automates this process. This approach does have some drawbacks: number|string and string|number are textually different but the same type. But so are string and any, despite being assignable to each other, which is really the point.
测试类型声明是一件棘手的事情。你应该测试它们。但要注意一些常见技术的陷阱,并考虑使用类似的工具dtslint来避免它们。
Testing type declarations is tricky business. You should test them. But be aware of the pitfalls of some of the common techniques and consider using a tool like dtslint to avoid them.
测试类型时,请注意相等性和可分配性之间的区别,尤其是对于函数类型。
When testing types, be aware of the difference between equality and assignability, particularly for function types.
对于使用回调的函数,测试回调参数的推断类型。如果它是您的 API 的一部分,请不要忘记测试它的类型this。
For functions that use callbacks, test the inferred types of the callback parameters. Don’t forget to test the type of this if it’s part of your API.
Be wary of any in tests involving types. Consider using a tool like dtslint for stricter, less error-prone checking.
本章内容有点多:它涵盖了编写代码(不是类型)时出现的一些问题以及运行代码时可能遇到的问题。
This chapter is a bit of a grab bag: it covers some issues that come up in writing code (not types) as well as issues you may run into when you run your code.
这随着时间的推移,TypeScript 和 JavaScript 之间的关系发生了变化。当 Microsoft 于 2010 年首次开始开发 TypeScript 时,人们普遍认为 JavaScript 是一种需要修复的有问题的语言。框架和源到源编译器通常会向 JavaScript 添加缺少的功能,例如类、装饰器和模块系统。TypeScript 也不例外。早期版本包括类、枚举和模块的本地版本。
The relationship between TypeScript and JavaScript has changed over time. When Microsoft first started work on TypeScript in 2010, the prevailing attitude around JavaScript was that it was a problematic language that needed to be fixed. It was common for frameworks and source-to-source compilers to add missing features like classes, decorators, and a module system to JavaScript. TypeScript was no different. Early versions included home-grown versions of classes, enums, and modules.
随着时间的推移,管理 JavaScript 的标准机构 TC39 将许多相同的功能添加到核心 JavaScript 语言中。他们添加的功能与 TypeScript 中现有的版本不兼容。这让 TypeScript 团队陷入了尴尬的困境:采用标准中的新功能还是破坏现有代码?
Over time TC39, the standards body that governs JavaScript, added many of these same features to the core JavaScript language. And the features they added were not compatible with the versions that existed in TypeScript. This left the TypeScript team in an awkward predicament: adopt the new features from the standard or break existing code?
TypeScript 在很大程度上选择了后者,并最终阐明了其当前的管理原则:TC39 定义运行时,而 TypeScript 仅在类型空间中进行创新。
TypeScript has largely chosen to do the latter and eventually articulated its current governing principle: TC39 defines the runtime while TypeScript innovates solely in the type space.
在此决定之前还有一些剩余功能。识别和理解这些很重要,因为它们不符合语言其余部分的模式。总的来说,我建议避免使用它们,以使 TypeScript 和 JavaScript 之间的关系尽可能清晰。
There are a few remaining features from before this decision. It’s important to recognize and understand these, because they don’t fit the pattern of the rest of the language. In general, I recommend avoiding them to keep the relationship between TypeScript and JavaScript as clear as possible.
许多语言模型类型可以使用枚举或枚举来获取一小组值。TypeScript 将它们添加到 JavaScript:
Many languages model types that can take on a small set of values using enumerations or enums. TypeScript adds them to JavaScript:
enumFlavor{VANILLA=0,CHOCOLATE=1,STRAWBERRY=2,}letflavor=Flavor.CHOCOLATE;// Type is FlavorFlavor// Autocomplete shows: VANILLA, CHOCOLATE, STRAWBERRYFlavor[0]// Value is "VANILLA"
enumFlavor{VANILLA=0,CHOCOLATE=1,STRAWBERRY=2,}letflavor=Flavor.CHOCOLATE;// Type is FlavorFlavor// Autocomplete shows: VANILLA, CHOCOLATE, STRAWBERRYFlavor[0]// Value is "VANILLA"
支持枚举的理由是它们比裸数字提供更多的安全性和透明度。但是 TypeScript 中的枚举有一些怪癖。实际上枚举有几种变体,它们的行为都略有不同:
The argument for enums is that they provide more safety and transparency than bare numbers. But enums in TypeScript have some quirks. There are actually several variants on enums that all have subtly different behaviors:
一个数值枚举(如Flavor)。任何数字都可以分配给它,所以它不是很安全。(以这种方式设计是为了使位标志结构成为可能。)
A number-valued enum (like Flavor). Any number is assignable to this, so it’s not very safe. (It was designed this way to make bit flag structures possible.)
字符串值枚举。这确实提供了类型安全,并且在运行时也提供了更透明的值。但它不是结构类型的,不像 TypeScript 中的所有其他类型(稍后会详细介绍)。
A string-valued enum. This does offer type safety, and also more transparent values at runtime. But it’s not structurally typed, unlike every other type in TypeScript (more on this momentarily).
const enum. 与常规枚举不同,const 枚举在运行时完全消失。const enum Flavor如果您在前面的示例中更改为,编译器将重写Flavor.CHOCOLATE为0. string这也打破了我们对编译器行为方式的预期,并且在和值枚举之间仍然具有不同的行为number。
const enum. Unlike regular enums, const enums go away completely at runtime. If you changed to const enum Flavor in the previous example, the compiler would rewrite Flavor.CHOCOLATE as 0. This also breaks our expectations around how the compiler behaves and still has the divergent behaviors between string and number-valued enums.
const enum设置preserveConstEnums标志。这会为const enums 发出运行时代码,就像对于常规enum.
const enum with the preserveConstEnums flag set. This emits runtime code for const enums, just like for a regular enum.
字符串值枚举在名义上是类型化的,这让人感到特别惊讶,因为 TypeScript 中的所有其他类型都使用结构化类型来实现可分配性(请参阅第 4 项):
That string-valued enums are nominally typed comes as a particular surprise, since every other type in TypeScript uses structural typing for assignability (see Item 4):
enumFlavor{VANILLA='vanilla',CHOCOLATE='chocolate',STRAWBERRY='strawberry',}letflavor=Flavor.CHOCOLATE;// Type is Flavorflavor='strawberry';// ~~~~~~ Type '"strawberry"' is not assignable to type 'Flavor'
enumFlavor{VANILLA='vanilla',CHOCOLATE='chocolate',STRAWBERRY='strawberry',}letflavor=Flavor.CHOCOLATE;// Type is Flavorflavor='strawberry';// ~~~~~~ Type '"strawberry"' is not assignable to type 'Flavor'
当您发布库时,这会产生影响。假设你有一个函数接受一个Flavor:
This has implications when you publish a library. Suppose you have a function that takes a Flavor:
functionscoop(flavor:Flavor){/* ... */}
functionscoop(flavor:Flavor){/* ... */}
因为 aFlavor在运行时实际上只是一个字符串,所以您的 JavaScript 用户可以用一个来调用它:
Because a Flavor at runtime is really just a string, it’s fine for your JavaScript users to call it with one:
scoop('vanilla');// OK in JavaScript
scoop('vanilla');// OK in JavaScript
但是您的 TypeScript 用户将需要导入enum并使用它:
but your TypeScript users will need to import the enum and use that instead:
scoop('vanilla');// ~~~~~~~~~ '"vanilla"' is not assignable to parameter of type 'Flavor'import{Flavor}from'ice-cream';scoop(Flavor.VANILLA);// OK
scoop('vanilla');// ~~~~~~~~~ '"vanilla"' is not assignable to parameter of type 'Flavor'import{Flavor}from'ice-cream';scoop(Flavor.VANILLA);// OK
JavaScript 和 TypeScript 用户的这些不同体验是避免使用字符串值枚举的原因。
These divergent experiences for JavaScript and TypeScript users are a reason to avoid string-valued enums.
TypeScript 提供了一种在其他语言中不太常见的枚举替代方法:文字类型的联合。
TypeScript offers an alternative to enums that is less common in other languages: a union of literal types.
typeFlavor='vanilla'|'chocolate'|'strawberry';letflavor:Flavor='chocolate';// OKflavor='mint chip';// ~~~~~~ Type '"mint chip"' is not assignable to type 'Flavor'
typeFlavor='vanilla'|'chocolate'|'strawberry';letflavor:Flavor='chocolate';// OKflavor='mint chip';// ~~~~~~ Type '"mint chip"' is not assignable to type 'Flavor'
这提供了与枚举一样多的安全性,并且具有更直接地转换为 JavaScript 的优势。它还在您的编辑器中提供了类似的强大自动完成功能:
This offers as much safety as the enum and has the advantage of translating more directly to JavaScript. It also offers similarly strong autocomplete in your editor:
functionscoop(flavor:Flavor){if(flavor==='v// Autocomplete here suggests 'vanilla'}
functionscoop(flavor:Flavor){if(flavor==='v// Autocomplete here suggests 'vanilla'}
有关此方法的更多信息,请参阅条目 33。
For more on this approach, see Item 33.
It’s common to assign properties to a constructor parameter when initializing a class:
classPerson{name:string;constructor(name:string){this.name=name;}}
classPerson{name:string;constructor(name:string){this.name=name;}}
TypeScript 为此提供了更紧凑的语法:
TypeScript provides a more compact syntax for this:
classPerson{constructor(publicname:string){}}
classPerson{constructor(publicname:string){}}
这称为“参数属性”,它等同于第一个示例中的代码。参数属性有几个问题需要注意:
This is called a “parameter property,” and it is equivalent to the code in the first example. There are a few issues to be aware of with parameter properties:
它们是为数不多的在编译为 JavaScript 时生成代码的构造之一(enums 是另一种)。通常编译只涉及擦除类型。
They are one of the few constructs which generates code when you compile to JavaScript (enums are another). Generally compilation just involves erasing types.
因为该参数仅在生成的代码中使用,所以源代码看起来好像有未使用的参数。
Because the parameter is only used in generated code, the source looks like it has unused parameters.
参数和非参数属性的混合可以隐藏类的设计。
A mix of parameter and non-parameter properties can hide the design of your classes.
例如:
For example:
classPerson{first:string;last:string;constructor(publicname:string){[this.first,this.last]=name.split(' ');}}
classPerson{first:string;last:string;constructor(publicname:string){[this.first,this.last]=name.split(' ');}}
此类具有三个属性 ( first, last, name),但这很难从代码中读出,因为在构造函数之前只列出了两个。如果构造函数也采用其他参数,情况会变得更糟。
This class has three properties (first, last, name), but this is hard to read off the code because only two are listed before the constructor. This gets worse if the constructor takes other parameters, too.
如果您的类仅包含参数属性而没有方法,您可以考虑将其设为interface并使用对象字面量。请记住,由于结构类型第 4 项,两者可以相互分配:
If your class consists only of parameter properties and no methods, you might consider making it an interface and using object literals. Remember that the two are assignable to one another because of structural typing Item 4:
classPerson{constructor(publicname:string){}}constp:Person={name:'Jed Bartlet'};// OK
classPerson{constructor(publicname:string){}}constp:Person={name:'Jed Bartlet'};// OK
关于参数属性的意见存在分歧。虽然我通常会避免使用它们,但其他人会喜欢节省的击键次数。请注意,它们不符合 TypeScript 其余部分的模式,并且实际上可能使新开发人员难以理解该模式。尽量避免通过混合使用参数和非参数属性来隐藏类的设计。
Opinions are divided on parameter properties. While I generally avoid them, others appreciate the saved keystrokes. Be aware that they do not fit the pattern of the rest of TypeScript, and may in fact obscure that pattern for new developers. Try to avoid hiding the design of your class by using a mix of parameter and non-parameter properties.
前ECMAScript 2015,JavaScript 没有官方的模块系统。不同的环境以不同的方式添加了这个缺失的功能:使用 Node.js require,module.exports而 AMD 使用define带有回调的函数。
Before ECMAScript 2015, JavaScript didn’t have an official module system. Different environments added this missing feature in different ways: Node.js used require and module.exports whereas AMD used a define function with a callback.
TypeScript 也用自己的模块系统填补了这一空白。这是使用module关键字和“三斜杠”导入完成的。在 ECMAScript 2015 添加了官方模块系统后,TypeScript 添加namespace为 的同义词module,以避免混淆:
TypeScript also filled this gap with its own module system. This was done using a module keyword and “triple-slash” imports. After ECMAScript 2015 added an official module system, TypeScript added namespace as a synonym for module, to avoid confusion:
namespacefoo{functionbar() {}}
namespacefoo{functionbar() {}}
/// <reference path="other.ts"/>foo.bar();
/// <reference path="other.ts"/>foo.bar();
在类型声明之外,三重斜杠导入和module关键字只是历史的好奇心。在您自己的代码中,您应该使用 ECMASCript 2015 风格的模块(import和export)。请参阅第 58 项。
Outside of type declarations, triple-slash imports and the module keyword are just a historical curiosity. In your own code, you should use ECMASCript 2015–style modules (import and export). See Item 58.
装饰器可用于注释或修改类、方法和属性。例如,您可以定义一个logged注解来记录对某个类的方法的所有调用:
Decorators can be used to annotate or modify classes, methods, and properties. For example, you could define a logged annotation that logs all calls to a method on a class:
classGreeter{greeting:string;constructor(message:string){this.greeting=message;}@loggedgreet() {return"Hello, "+this.greeting;}}functionlogged(target:any,name:string,descriptor:PropertyDescriptor){constfn=target[name];descriptor.value=function(){console.log(`Calling${name}`);returnfn.apply(this,arguments);};}console.log(newGreeter('Dave').greet());// Logs:// Calling greet// Hello, Dave
classGreeter{greeting:string;constructor(message:string){this.greeting=message;}@loggedgreet() {return"Hello, "+this.greeting;}}functionlogged(target:any,name:string,descriptor:PropertyDescriptor){constfn=target[name];descriptor.value=function(){console.log(`Calling${name}`);returnfn.apply(this,arguments);};}console.log(newGreeter('Dave').greet());// Logs:// Calling greet// Hello, Dave
这最初添加功能是为了支持Angular 框架并要求experimentalDecorators在tsconfig.json. 在撰写本文时,它们的实现尚未由 TC39 标准化,因此您今天使用装饰器编写的任何代码将来都可能会被破坏或变得不标准。除非您使用的是 Angular 或其他需要注释的框架,并且在它们被标准化之前,否则不要使用 TypeScript 的装饰器。
This feature was initially added to support the Angular framework and requires the experimentalDecorators property to be set in tsconfig.json. Their implementation has not yet been standardized by TC39 at the time of this writing, so any code you write today using decorators is liable to break or become non-standard in the future. Unless you’re using Angular or another framework that requires annotations and until they’re standardized, don’t use TypeScript’s decorators.
总的来说,您可以通过从代码中删除所有类型来将 TypeScript 转换为 JavaScript。
By and large, you can convert TypeScript to JavaScript by removing all the types from your code.
枚举、参数属性、三重斜杠导入和装饰器是该规则的历史例外。
Enums, parameter properties, triple-slash imports, and decorators are historical exceptions to this rule.
In order to keep TypeScript’s role in your codebase as clear as possible, I recommend avoiding these features.
这代码运行良好,但 TypeScript 会在其中标记错误。为什么?
This code runs fine, and yet TypeScript flags an error in it. Why?
constobj={one:'uno',two:'dos',three:'tres',};for(constkinobj){constv=obj[k];// ~~~~~~ Element implicitly has an 'any' type// because type ... has no index signature}
constobj={one:'uno',two:'dos',three:'tres',};for(constkinobj){constv=obj[k];// ~~~~~~ Element implicitly has an 'any' type// because type ... has no index signature}
检查obj和k符号给出了一个线索:
Inspecting the obj and k symbols gives a clue:
constobj={/* ... */};// const obj: {// one: string;// two: string;// three: string;// }for(constkinobj){// const k: string// ...}
constobj={/* ... */};// const obj: {// one: string;// two: string;// three: string;// }for(constkinobj){// const k: string// ...}
的类型k是string,但您试图索引一个对象,该对象的类型只有三个特定键:'one'、'two'和'three'。除了这三个还有其他字符串,所以这必须失败。
The type of k is string, but you’re trying to index into an object whose type only has three specific keys: 'one', 'two', and 'three'. There are strings other than these three, so this has to fail.
插入一个更窄的类型声明来k解决这个问题:
Plugging in a narrower type declaration for k fixes the issue:
letk:keyoftypeofobj;// Type is "one" | "two" | "three"for(kinobj){constv=obj[k];// OK}
letk:keyoftypeofobj;// Type is "one" | "two" | "three"for(kinobj){constv=obj[k];// OK}
k所以真正的问题是:为什么第一个例子中的类型被推断为string而不是"one" | "two" | "three"?
So the real question is: why is the type of k in the first example inferred as string rather than "one" | "two" | "three"?
为了理解,让我们看一个稍微不同的例子,涉及一个接口和一个函数:
To understand, let’s look at a slightly different example involving an interface and a function:
interfaceABC{a:string;b:string;c:number;}functionfoo(abc:ABC){for(constkinabc){// const k: stringconstv=abc[k];// ~~~~~~ Element implicitly has an 'any' type// because type 'ABC' has no index signature}}
interfaceABC{a:string;b:string;c:number;}functionfoo(abc:ABC){for(constkinabc){// const k: stringconstv=abc[k];// ~~~~~~ Element implicitly has an 'any' type// because type 'ABC' has no index signature}}
这是和以前一样的错误。您可以使用相同类型的声明 ( let k: keyof ABC) 来“修复”它。但在这种情况下,TypeScript 的抱怨是对的。原因如下:
It’s the same error as before. And you can “fix” it using the same sort of declaration (let k: keyof ABC). But in this case TypeScript is right to complain. Here’s why:
constx={a:'a',b:'b',c:2,d:newDate()};foo(x);// OK
constx={a:'a',b:'b',c:2,d:newDate()};foo(x);// OK
这可以使用可分配给 的任何值来foo调用函数,而不仅仅是具有“a”、“b”和“c”属性的值。该值也完全有可能具有其他属性(请参阅第 4 项)。为了实现这一点,TypeScript 提供了唯一可以确定的类型,即.ABCkstring
The function foo can be called with any value assignable to ABC, not just a value with “a,” “b,” and “c” properties. It’s entirely possible that the value will have other properties, too (see Item 4). To allow for this, TypeScript gives k the only type it can be confident of, namely, string.
使用keyof声明在这里还有另一个缺点:
Using the keyof declaration would have another downside here:
functionfoo(abc:ABC){letk:keyofABC;for(kinabc){// let k: "a" | "b" | "c"constv=abc[k];// Type is string | number}}
functionfoo(abc:ABC){letk:keyofABC;for(kinabc){// let k: "a" | "b" | "c"constv=abc[k];// Type is string | number}}
如果"a" | "b" | "c"is too narrow for k,那么string | number肯定是 too narrow for v。在前面的示例中,其中一个值是 a Date,但它可以是任何值。这里的类型给人一种错误的确定感,可能会导致运行时出现混乱。
If "a" | "b" | "c" is too narrow for k, then string | number is certainly too narrow for v. In the preceding example one of the values is a Date, but it could be anything. The types here give a false sense of certainty that could lead to chaos at runtime.
那么,如果您只想遍历对象的键和值而不会出现类型错误怎么办?Object.entries让你同时迭代两者:
So what if you just want to iterate over the object’s keys and values without type errors? Object.entries lets you iterate over both simultaneously:
functionfoo(abc:ABC){for(const[k,v]ofObject.entries(abc)){k// Type is stringv// Type is any}}
functionfoo(abc:ABC){for(const[k,v]ofObject.entries(abc)){k// Type is stringv// Type is any}}
虽然这些类型可能很难共事,但他们至少是诚实的!
While these types may be hard to work with, they are at least honest!
你还应注意样机污染的可能性。即使在您定义的对象字面量的情况下,for-in 也可以生成额外的键:
You should also be aware of the possibility of prototype pollution. Even in the case of an object literal that you define, for-in can produce additional keys:
>对象.prototype.z = 3; // 请不要这样做!
> const obj = {x: 1, y: 2};
> for (const k in obj) { console.log(k); }
X
是
z> Object.prototype.z = 3; // Please don't do this!
> const obj = {x: 1, y: 2};
> for (const k in obj) { console.log(k); }
x
y
z
希望这不会发生在非对抗性环境中(你永远不应该向 中添加可枚举属性Object.prototype),但这是 for-instring甚至为对象文字生成键的另一个原因。
Hopefully this doesn’t happen in a nonadversarial environment (you should never add enumerable properties to Object.prototype), but it is another reason that for-in produces string keys even for object literals.
如果要遍历对象中的键和值,请使用keyof声明 ( let k: keyof T) 或Object.entries. 前者适用于常量或您知道对象不会有额外键并且您需要精确类型的其他情况。后者更普遍,尽管键和值类型更难处理。
If you want to iterate over the keys and values in an object, use either a keyof declaration (let k: keyof T) or Object.entries. The former is appropriate for constants or other situations where you know that the object won’t have additional keys and you want precise types. The latter is more generally appropriate, though the key and value types are more difficult to work with.
let k: keyof T当您确切知道键是什么时,使用 for-in 循环来迭代对象。请注意,您的函数作为参数接收的任何对象都可能具有其他键。
Use let k: keyof T and a for-in loop to iterate objects when you know exactly what the keys will be. Be aware that any objects your function receives as parameters might have additional keys.
用于Object.entries迭代任何对象的键和值。
Use Object.entries to iterate over the keys and values of any object.
最多本书中的项目与您运行 TypeScript 的位置无关:在 Web 浏览器中、在服务器上、在手机上。这个是不同的。如果您不在浏览器中工作,请跳过!
Most of the items in this book are agnostic about where you run your TypeScript: in a web browser, on a server, on a phone. This one is different. If you’re not working in a browser, skip ahead!
当您在 Web 浏览器中运行 JavaScript 时,DOM 层次结构始终存在。当您使用document.getElementById获取元素或document.createElement创建元素时,它始终是一种特定类型的元素,即使您并不完全熟悉分类法。您调用这些方法并使用您想要的属性并希望获得最好的结果。
The DOM hierarchy is always present when you’re running JavaScript in a web browser. When you use document.getElementById to get an element or document.createElement to create one, it’s always a particular kind of element, even if you’re not entirely familiar with the taxonomy. You call the methods and use the properties that you want and hope for the best.
和TypeScript,DOM 元素的层次结构变得更加明显。Node从你的 sElement和s 中了解你的EventTargets 将帮助你调试类型错误并决定何时类型断言是合适的。因为很多 API 都基于 DOM,所以即使您使用的是像这样的框架,这也是相关的反应或 d3。
With TypeScript, the hierarchy of DOM elements becomes more visible. Knowing your Nodes from your Elements and EventTargets will help you debug type errors and decide when type assertions are appropriate. Because so many APIs are based on the DOM, this is relevant even if you’re using a framework like React or d3.
假设您想要在用户将鼠标拖过<div>. 你写了一些看似无害的 JavaScript:
Suppose you want to track a user’s mouse as they drag it across a <div>. You write some seemingly innocuous JavaScript:
functionhandleDrag(eDown:Event){consttargetEl=eDown.currentTarget;targetEl.classList.add('dragging');constdragStart=[eDown.clientX,eDown.clientY];consthandleUp=(eUp:Event)=>{targetEl.classList.remove('dragging');targetEl.removeEventListener('mouseup',handleUp);constdragEnd=[eUp.clientX,eUp.clientY];console.log('dx, dy = ',[0,1].map(i=>dragEnd[i]-dragStart[i]));}targetEl.addEventListener('mouseup',handleUp);}constdiv=document.getElementById('surface');div.addEventListener('mousedown',handleDrag);
functionhandleDrag(eDown:Event){consttargetEl=eDown.currentTarget;targetEl.classList.add('dragging');constdragStart=[eDown.clientX,eDown.clientY];consthandleUp=(eUp:Event)=>{targetEl.classList.remove('dragging');targetEl.removeEventListener('mouseup',handleUp);constdragEnd=[eUp.clientX,eUp.clientY];console.log('dx, dy = ',[0,1].map(i=>dragEnd[i]-dragStart[i]));}targetEl.addEventListener('mouseup',handleUp);}constdiv=document.getElementById('surface');div.addEventListener('mousedown',handleDrag);
TypeScript 的类型检查器在这 14 行代码中标记了不少于 11 个错误:
TypeScript’s type checker flags no fewer than 11 errors in these 14 lines of code:
functionhandleDrag(eDown:Event){consttargetEl=eDown.currentTarget;targetEl.classList.add('dragging');// ~~~~~~~ Object is possibly 'null'.// ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'constdragStart=[eDown.clientX,eDown.clientY];// ~~~~~~~ Property 'clientX' does not exist on 'Event'// ~~~~~~~ Property 'clientY' does not exist on 'Event'consthandleUp=(eUp:Event)=>{targetEl.classList.remove('dragging');// ~~~~~~~~ Object is possibly 'null'.// ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'targetEl.removeEventListener('mouseup',handleUp);// ~~~~~~~~ Object is possibly 'null'constdragEnd=[eUp.clientX,eUp.clientY];// ~~~~~~~ Property 'clientX' does not exist on 'Event'// ~~~~~~~ Property 'clientY' does not exist on 'Event'console.log('dx, dy = ',[0,1].map(i=>dragEnd[i]-dragStart[i]));}targetEl.addEventListener('mouseup',handleUp);// ~~~~~~~ Object is possibly 'null'}constdiv=document.getElementById('surface');div.addEventListener('mousedown',handleDrag);// ~~~ Object is possibly 'null'
functionhandleDrag(eDown:Event){consttargetEl=eDown.currentTarget;targetEl.classList.add('dragging');// ~~~~~~~ Object is possibly 'null'.// ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'constdragStart=[eDown.clientX,eDown.clientY];// ~~~~~~~ Property 'clientX' does not exist on 'Event'// ~~~~~~~ Property 'clientY' does not exist on 'Event'consthandleUp=(eUp:Event)=>{targetEl.classList.remove('dragging');// ~~~~~~~~ Object is possibly 'null'.// ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'targetEl.removeEventListener('mouseup',handleUp);// ~~~~~~~~ Object is possibly 'null'constdragEnd=[eUp.clientX,eUp.clientY];// ~~~~~~~ Property 'clientX' does not exist on 'Event'// ~~~~~~~ Property 'clientY' does not exist on 'Event'console.log('dx, dy = ',[0,1].map(i=>dragEnd[i]-dragStart[i]));}targetEl.addEventListener('mouseup',handleUp);// ~~~~~~~ Object is possibly 'null'}constdiv=document.getElementById('surface');div.addEventListener('mousedown',handleDrag);// ~~~ Object is possibly 'null'
什么地方出了错?这是什么EventTarget?为什么一切都可能是null?
What went wrong? What’s this EventTarget? And why might everything be null?
要了解EventTarget错误,深入了解 DOM 层次结构会有所帮助。这是一些 HTML:
To understand the EventTarget errors it helps to dig into the DOM hierarchy a bit. Here’s some HTML:
<pid="quote">但<i>它</i>移动</p>
<pid="quote">and<i>yet</i>it moves</p>
如果您打开浏览器的 JavaScript 控制台并获得对该元素的引用p,您会看到它是一个HTMLParagraphElement:
If you open your browser’s JavaScript console and get a reference to the p element, you’ll see that it’s an HTMLParagraphElement:
constp=document.getElementsByTagName('p')[0];pinstanceofHTMLParagraphElement// True
constp=document.getElementsByTagName('p')[0];pinstanceofHTMLParagraphElement// True
一个HTMLParagraphElement是的子类型HTMLElement,它是 的子类型Element,它是 的子类型Node,它是 的子类型EventTarget。以下是层次结构中类型的一些示例:
An HTMLParagraphElement is a subtype of HTMLElement, which is a subtype of Element, which is a subtype of Node, which is a subtype of EventTarget. Here are some examples of types along the hierarchy:
| 类型 | 例子 |
|---|---|
事件目标 EventTarget |
|
节点 Node |
|
元素 Element |
包括 HTMLElements、SVGElements includes HTMLElements, SVGElements |
HTML元素 HTMLElement |
|
HTML按钮元素 HTMLButtonElement |
|
AnEventTarget是最通用的 DOM 类型。您所能做的就是添加事件侦听器、删除它们和分发事件。考虑到这一点,classList错误开始变得更有意义了:
An EventTarget is the most generic of DOM types. All you can do with it is add event listeners, remove them, and dispatch events. With this in mind, the classList errors start to make a bit more sense:
functionhandleDrag(eDown:Event){consttargetEl=eDown.currentTarget;targetEl.classList.add('dragging');// ~~~~~~~ Object is possibly 'null'// ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'// ...}
functionhandleDrag(eDown:Event){consttargetEl=eDown.currentTarget;targetEl.classList.add('dragging');// ~~~~~~~ Object is possibly 'null'// ~~~~~~~~~ Property 'classList' does not exist on type 'EventTarget'// ...}
顾名思义, anEvent的currentTarget属性是EventTarget. 它甚至可能是null。TypeScript 没有理由相信它有一个classList属性。虽然EventTargets实际上可以是 an HTMLElement,但从类型系统的角度来看,没有理由不能是windowor XMLHTTPRequest。
As its name implies, an Event’s currentTarget property is an EventTarget. It could even be null. TypeScript has no reason to believe that it has a classList property. While an EventTargets could be an HTMLElement in practice, from the type system’s perspective there’s no reason it couldn’t be window or XMLHTTPRequest.
向上移动我们来到的层次结构Node。Nodes 不是s的几个例子Element是文本片段和评论。例如,在这个 HTML 中:
Moving up the hierarchy we come to Node. A couple of examples of Nodes that are not Elements are text fragments and comments. For instance, in this HTML:
<p>然而它动<i>了</i><!-- quote from Galileo --></p>
<p>And<i>yet</i>it moves<!-- quote from Galileo --></p>
最外层的元素是一个HTMLParagraphElement. 正如你在这里看到的,它有children和childNodes:
the outermost element is an HTMLParagraphElement. As you can see here, it has children and childNodes:
> p.children HTML 集合 [i] > p.childNodes NodeList(5) [text, i, text, comment, text]
> p.children HTMLCollection [i] > p.childNodes NodeList(5) [text, i, text, comment, text]
children返回一个HTMLCollection,一个类似数组的结构,只包含子Elements ( <i>yet</i>)。childNodes返回一个sNodeList的类数组集合Node。这不仅包括Elements ( <i>yet</i>),还包括文本片段(“And”、“it moves”)和评论(“quote from Galileo”)。
children returns an HTMLCollection, an array-like structure containing just the child Elements (<i>yet</i>). childNodes returns a NodeList, an Array-like collection of Nodes. This includes not just Elements (<i>yet</i>) but also text fragments (“And,” “it moves”) and comments (“quote from Galileo”).
Elementan和 an 和有什么不一样HTMLElement?有非 HTMLElement包括 SVG 标签的整个层次结构。这些是SVGElements,是另一种类型的Element. <html>or标签的类型是什么<svg>?他们是HTMLHtmlElement和SVGSvgElement。
What’s the difference between an Element and an HTMLElement? There are non-HTML Elements including the whole hierarchy of SVG tags. These are SVGElements, which are another type of Element. What’s the type of an <html> or <svg> tag? They’re HTMLHtmlElement and SVGSvgElement.
有时这些专门化的类会有自己的属性——例如,一个HTMLImageElement有一个src属性,一个HTMLInputElement有一个value属性。如果你想从一个值中读取这些属性之一,它的类型必须足够具体以具有该属性。
Sometimes these specialized classes will have properties of their own—for example, an HTMLImageElement has a src property, and an HTMLInputElement has a value property. If you want to read one of these properties off a value, its type must be specific enough to have that property.
TypeScript 的 DOM 类型声明自由使用字面量类型来尝试为您提供最具体的类型。例如:
TypeScript’s type declarations for the DOM make liberal use of literal types to try to get you the most specific type possible. For example:
document.getElementsByTagName('p')[0];// HTMLParagraphElementdocument.createElement('button');// HTMLButtonElementdocument.querySelector('div');// HTMLDivElement
document.getElementsByTagName('p')[0];// HTMLParagraphElementdocument.createElement('button');// HTMLButtonElementdocument.querySelector('div');// HTMLDivElement
但这并不总是可能的,尤其是document.getElementById:
but this is not always possible, notably with document.getElementById:
document.getElementById('my-div');// HTMLElement
document.getElementById('my-div');// HTMLElement
虽然类型断言通常不受欢迎(第 9 项),但在这种情况下,您比 TypeScript 知道的更多,因此它们是合适的。这没什么问题,只要你知道那#my-div是一个 div:
While type assertions are generally frowned upon (Item 9), this is a case where you know more than TypeScript does and so they are appropriate. There’s nothing wrong with this, so long as you know that #my-div is a div:
document.getElementById('my-div')asHTMLDivElement;
document.getElementById('my-div')asHTMLDivElement;
启用后,您将需要考虑返回的strictNullChecks情况。根据这是否真的会发生,您可以添加 if 语句或断言 ( ):document.getElementByIdnull!
with strictNullChecks enabled, you will need to consider the case that document.getElementById returns null. Depending on whether this can really happen, you can either add an if statement or an assertion (!):
constdiv=document.getElementById('my-div')!;
constdiv=document.getElementById('my-div')!;
这些类型并不特定于 TypeScript。相反,它们是从 DOM 的正式规范生成的。这是Item 35建议尽可能从规范生成类型的示例。
These types are not specific to TypeScript. Rather, they are generated from the formal specification of the DOM. This is an example of the advice of Item 35 to generate types from specs when possible.
DOM 层次结构就这么多了。clientX和错误呢clientY?
So much for the DOM hierarchy. What about the clientX and clientY errors?
functionhandleDrag(eDown:Event){// ...constdragStart=[eDown.clientX,eDown.clientY];// ~~~~~~~ Property 'clientX' does not exist on 'Event'// ~~~~~~~ Property 'clientY' does not exist on 'Event'// ...}
functionhandleDrag(eDown:Event){// ...constdragStart=[eDown.clientX,eDown.clientY];// ~~~~~~~ Property 'clientX' does not exist on 'Event'// ~~~~~~~ Property 'clientY' does not exist on 'Event'// ...}
在除了 hierarchy for Nodes 和Elements 之外,还有一个 hierarchy for Events。Mozilla 文档目前列出不少于 52 种类型的Event!
In addition to the hierarchy for Nodes and Elements, there is also a hierarchy for Events. The Mozilla documentation currently lists no fewer than 52 types of Event!
Plain Event is the most generic type of event. More specific types include:
UIEventUIEvent任何类型的用户界面事件
Any sort of user interface event
MouseEventMouseEvent由鼠标触发的事件,例如单击
An event triggered by the mouse such as a click
TouchEventTouchEvent移动设备上的触摸事件
A touch event on a mobile device
WheelEventWheelEvent旋转滚轮触发的事件
An event triggered by rotating the scroll wheel
KeyboardEventKeyboardEvent一个按键
A key press
中的问题handleDrag是事件被声明为Event,而clientX和clientY仅存在于更具体的MouseEvent类型上。
The problem in handleDrag is that the events are declared as Event, while clientX and clientY exist only on the more specific MouseEvent type.
那么如何从本项目的开头修复示例呢?TypeScript 的 DOM 声明广泛使用了上下文(条目 26)。内联 mousedown 处理程序为 TypeScript 提供了更多可使用的信息并消除了大部分错误。您还可以将参数类型声明为MouseEvent而不是Event。这是一个使用这两种技术来修复错误的版本:
So how can you fix the example from the start of this item? TypeScript’s declarations for the DOM make extensive use of context (Item 26). Inlining the mousedown handler gives TypeScript more information to work with and removes most of the errors. You can also declare the parameter type to be MouseEvent rather than Event. Here’s a version that uses both techniques to fix the errors:
functionaddDragHandler(el:HTMLElement){el.addEventListener('mousedown',eDown=>{constdragStart=[eDown.clientX,eDown.clientY];consthandleUp=(eUp:MouseEvent)=>{el.classList.remove('dragging');el.removeEventListener('mouseup',handleUp);constdragEnd=[eUp.clientX,eUp.clientY];console.log('dx, dy = ',[0,1].map(i=>dragEnd[i]-dragStart[i]));}el.addEventListener('mouseup',handleUp);});}constdiv=document.getElementById('surface');if(div){addDragHandler(div);}
functionaddDragHandler(el:HTMLElement){el.addEventListener('mousedown',eDown=>{constdragStart=[eDown.clientX,eDown.clientY];consthandleUp=(eUp:MouseEvent)=>{el.classList.remove('dragging');el.removeEventListener('mouseup',handleUp);constdragEnd=[eUp.clientX,eUp.clientY];console.log('dx, dy = ',[0,1].map(i=>dragEnd[i]-dragStart[i]));}el.addEventListener('mouseup',handleUp);});}constdiv=document.getElementById('surface');if(div){addDragHandler(div);}
最后的语句if处理没有#surface元素的可能性。如果您知道此元素存在,则可以改用断言 ( div!)。addDragHandler需要一个 non-null ,所以这是一个将值推送到边界HTMLElement的示例(条目 31)。null
The if statement at the end handles the possibility that there is no #surface element. If you know that this element exists, you could use an assertion instead (div!). addDragHandler requires a non-null HTMLElement, so this is an example of pushing null values to the perimeter (Item 31).
DOM 有一个类型层次结构,您在编写 JavaScript 时通常可以忽略它。但是这些类型在 TypeScript 中变得更加重要。了解它们将帮助您为浏览器编写 TypeScript。
The DOM has a type hierarchy that you can usually ignore while writing JavaScript. But these types become more important in TypeScript. Understanding them will help you write TypeScript for the browser.
Node了解、Element、HTMLElement和 之间的区别EventTarget,以及Event和之间的区别MouseEvent。
Know the differences between Node, Element, HTMLElement, and EventTarget, as well as those between Event and MouseEvent.
Either use a specific enough type for DOM elements and Events in your code or give TypeScript the context to infer it.
JavaScript历史上一直缺乏一种方法来将类的属性设为私有。通常的解决方法是使用下划线为不属于公共 API 的字段添加前缀:
JavaScript has historically lacked a way to make properties of a class private. The usual workaround is a convention of prefixing fields that are not part of a public API with underscores:
classFoo{_private='secret123';}
classFoo{_private='secret123';}
但这只会阻止用户访问私人数据。很容易规避:
But this only discourages users from accessing private data. It is easy to circumvent:
constf=newFoo();f._private;// 'secret123'
constf=newFoo();f._private;// 'secret123'
TypeScript 添加了public、protected和private字段修饰符,它们似乎提供了一些强制执行:
TypeScript adds public, protected, and private field modifiers that seem to provide some enforcement:
classDiary{privatesecret='cheated on my English test';}constdiary=newDiary();diary.secret// ~~~~~~ Property 'secret' is private and only// accessible within class 'Diary'
classDiary{privatesecret='cheated on my English test';}constdiary=newDiary();diary.secret// ~~~~~~ Property 'secret' is private and only// accessible within class 'Diary'
Butprivate是类型系统的一个特性,并且像类型系统的所有特性一样,它会在运行时消失(参见Item 3)。这是当 TypeScript 将它编译成 JavaScript 时,这段代码是什么样子的(使用target=ES2017):
But private is a feature of the type system and, like all features of the type system, it goes away at runtime (see Item 3). Here’s what this snippet looks like when TypeScript compiles it to JavaScript (with target=ES2017):
classDiary{constructor(){this.secret='cheated on my English test';}}constdiary=newDiary();diary.secret;
classDiary{constructor(){this.secret='cheated on my English test';}}constdiary=newDiary();diary.secret;
指示器private不见了,你的秘密就泄露了!与惯例非常相似_private,TypeScript 的访问修饰符只会阻止您访问私有数据。通过类型断言,您甚至可以从 TypeScript 中访问私有属性:
The private indicator is gone, and your secret is out! Much like the _private convention, TypeScript’s access modifiers only discourage you from accessing private data. With a type assertion, you can even access a private property from within TypeScript:
classDiary{privatesecret='cheated on my English test';}constdiary=newDiary();(diaryasany).secret// OK
classDiary{privatesecret='cheated on my English test';}constdiary=newDiary();(diaryasany).secret// OK
换句话说,不要靠private隐藏信息!
In other words, don’t rely on private to hide information!
所以如果你想要更强大的东西,你应该怎么做?传统的答案是利用 JavaScript 最可靠的方法之一来隐藏信息:闭包。您可以在构造函数中创建一个:
So what should you do if you want something more robust? The traditional answer has been to take advantage of one of JavaScript’s most reliable ways to hide information: closures. You can create one in a constructor:
declarefunctionhash(text:string):number;classPasswordChecker{checkPassword:(password:string)=>boolean;constructor(passwordHash:number){this.checkPassword=(password:string)=>{returnhash(password)===passwordHash;}}}constchecker=newPasswordChecker(hash('s3cret'));checker.checkPassword('s3cret');// Returns true
declarefunctionhash(text:string):number;classPasswordChecker{checkPassword:(password:string)=>boolean;constructor(passwordHash:number){this.checkPassword=(password:string)=>{returnhash(password)===passwordHash;}}}constchecker=newPasswordChecker(hash('s3cret'));checker.checkPassword('s3cret');// Returns true
JavaScript 不提供passwordHash从PasswordChecker. 然而,这确实有一些缺点:具体来说,因为passwordHash在构造函数之外看不到,所以每个使用它的方法也必须在那里定义。这会导致为每个类实例创建每个方法的副本,这将导致更高的内存使用。它还可以防止同一类的其他实例访问私有数据。关闭可能会带来不便,但它们肯定会使您的数据保密!
JavaScript offers no way to access the passwordHash variable from outside of the constructor of PasswordChecker. This does have a few downsides, however: specifically, because passwordHash can’t be seen outside the constructor, every method that uses it also has to be defined there. This results in a copy of each method being created for every class instance, which will lead to higher memory use. It also prevents other instances of the same class from accessing private data. Closures may be inconvenient, but they will certainly keep your data private!
A较新的选择是使用私有域,这是一个被提议的语言特性,随着本书的印刷,它正在得到巩固。在此提案中,要使一个字段在类型检查和运行时都是私有的,请在其前面加上前缀#:
A newer option is to use private fields, a proposed language feature that is solidifying as this book goes to print. In this proposal, to make a field private both for type checking and at runtime, prefix it with a #:
classPasswordChecker{#passwordHash:number;constructor(passwordHash:number){this.#passwordHash=passwordHash;}checkPassword(password:string){returnhash(password)===this.#passwordHash;}}constchecker=newPasswordChecker(hash('s3cret'));checker.checkPassword('secret');// Returns falsechecker.checkPassword('s3cret');// Returns true
classPasswordChecker{#passwordHash:number;constructor(passwordHash:number){this.#passwordHash=passwordHash;}checkPassword(password:string){returnhash(password)===this.#passwordHash;}}constchecker=newPasswordChecker(hash('s3cret'));checker.checkPassword('secret');// Returns falsechecker.checkPassword('s3cret');// Returns true
该#passwordHash属性不能从类外访问。与闭包技术相反,它可以从类方法和同一类的其他实例访问。对于本身不支持私有字段的 ECMAScript 目标,使用WeakMaps 的后备实现代替。结果是您的数据仍然是私有的。该提案处于第 3 阶段,随着本书的印刷,TypeScript 也得到了支持。如果您想使用它,请查看 TypeScript 发行说明以查看它是否普遍可用。
The #passwordHash property is not accessible from outside the class. In contrast to the closure technique, it is accessible from class methods and from other instances of the same class. For ECMAScript targets that don’t natively support private fields, a fallback implementation using WeakMaps is used instead. The upshot is that your data is still private. This proposal was stage 3 and support was being added to TypeScript as this book went to print. If you’d like to use it, check the TypeScript release notes to see if it’s generally available.
最后,如果您担心安全性,而不仅仅是封装,那么还有其他需要注意的问题,例如对内置原型和功能的修改。
Finally, if you are worried about security, rather than just encapsulation, then there are others concerns to be aware of such as modifications to built-in prototypes and functions.
访问private修饰符仅通过类型系统强制执行。它在运行时没有影响,可以通过断言绕过。不要假设它会隐藏数据。
The private access modifier is only enforced through the type system. It has no effect at runtime and can be bypassed with an assertion. Don’t assume it will keep data hidden.
为了更可靠的信息隐藏,使用闭包。
For more reliable information hiding, use a closure.
什么时候你运行 TypeScript 代码,你实际上是在运行 TypeScript 编译器生成的 JavaScript。任何源到源编译器都是如此,无论是压缩器、编译器还是预处理器。希望这大部分是透明的,你可以假装正在执行 TypeScript 源代码,而无需查看 JavaScript。
When you run TypeScript code, you’re actually running the JavaScript that the TypeScript compiler generates. This is true of any source-to-source compiler, be it a minifier, a compiler, or a preprocessor. The hope is that this is mostly transparent, that you can pretend that the TypeScript source code is being executed without ever having to look at the JavaScript.
在您必须调试代码之前,此方法运行良好。调试器通常处理您正在执行的代码,并且不知道它所经历的翻译过程。由于 JavaScript 是一种非常流行的目标语言,因此浏览器供应商合作解决了这个问题。结果是源地图。它们将生成的文件中的位置和符号映射回原始源中的相应位置和符号。大多数浏览器和许多 IDE 都支持它们。如果你不使用它们来调试你的 TypeScript,那你就错过了!
This works well until you have to debug your code. Debuggers generally work on the code you’re executing and don’t know about the translation process it went through. Since JavaScript is such a popular target language, browser vendors collaborated to solve this problem. The result is source maps. They map positions and symbols in a generated file back to the corresponding positions and symbols in the original source. Most browsers and many IDEs support them. If you’re not using them to debug your TypeScript, you’re missing out!
假设您已经创建了一个小脚本来向 HTML 页面添加一个按钮,该按钮会在您每次单击时递增:
Suppose you’ve created a small script to add a button to an HTML page that increments every time you click it:
functionaddCounter(el:HTMLElement){letclickCount=0;constbutton=document.createElement('button');button.textContent='Click me';button.addEventListener('click',()=>{clickCount++;button.textContent=`Click me (${clickCount})`;});el.appendChild(button);}addCounter(document.body);
functionaddCounter(el:HTMLElement){letclickCount=0;constbutton=document.createElement('button');button.textContent='Click me';button.addEventListener('click',()=>{clickCount++;button.textContent=`Click me (${clickCount})`;});el.appendChild(button);}addCounter(document.body);
如果您在浏览器中加载它并打开调试器,您将看到生成的 JavaScript。这与原始源非常匹配,因此调试并不太困难,如图7-1所示。
If you load this in your browser and open the debugger, you’ll see the generated JavaScript. This closely matches the original source, so debugging isn’t too difficult, as you can see in Figure 7-1.
让我们通过从 numbersapi.com 获取关于每个数字的有趣事实来使页面更有趣:
Let’s make the page more fun by fetching an interesting fact about each number from numbersapi.com:
functionaddCounter(el:HTMLElement){letclickCount=0;consttriviaEl=document.createElement('p');constbutton=document.createElement('button');button.textContent='Click me';button.addEventListener('click',async()=>{clickCount++;constresponse=awaitfetch(`http://numbersapi.com/${clickCount}`);consttrivia=awaitresponse.text();triviaEl.textContent=trivia;button.textContent=`Click me (${clickCount})`;});el.appendChild(triviaEl);el.appendChild(button);}
functionaddCounter(el:HTMLElement){letclickCount=0;consttriviaEl=document.createElement('p');constbutton=document.createElement('button');button.textContent='Click me';button.addEventListener('click',async()=>{clickCount++;constresponse=awaitfetch(`http://numbersapi.com/${clickCount}`);consttrivia=awaitresponse.text();triviaEl.textContent=trivia;button.textContent=`Click me (${clickCount})`;});el.appendChild(triviaEl);el.appendChild(button);}
如果您现在打开浏览器的调试器,您会看到生成的源代码变得非常复杂(见图7-2)。
If you open up your browser’s debugger now, you’ll see that the generated source has gotten dramatically more complicated (see Figure 7-2).
为了支持async旧版await浏览器,TypeScript 将事件处理程序重写为状态机。这具有相同的行为,但代码不再与原始源代码如此相似。
To support async and await in older browsers, TypeScript has rewritten the event handler as a state machine. This has the same behavior, but the code no longer bears such a close resemblance to the original source.
这是源映射可以提供帮助的地方。要告诉 TypeScript 生成一个,请在tsconfig.jsonsourceMap中设置选项:
This is where source maps can help. To tell TypeScript to generate one, set the sourceMap option in your tsconfig.json:
{"compilerOptions":{"sourceMap":true}}
{"compilerOptions":{"sourceMap":true}}
现在,当您运行时tsc,它会为每个.ts文件生成两个输出文件:一个.js文件和一个.js.map文件。后者是源地图。
Now when you run tsc, it generates two output files for each .ts file: a .js file and a .js.map file. The latter is the source map.
有了这个文件,一个新的index.ts文件就会出现在浏览器的调试器中。您可以在其中设置断点并检查变量,正如您希望的那样(参见图 7-3)。
With this file in place, a new index.ts file appears in your browser’s debugger. You can set breakpoints and inspect variables in it, just as you’d hope (see Figure 7-3).
请注意,index.ts在左侧的文件列表中以斜体显示。这表明它不是网页包含它的意义上的“真实”文件。相反,它是通过源地图包含的。根据您的设置,index.js.map将包含对index.ts的引用(在这种情况下浏览器通过网络加载它)或它的内联副本(在这种情况下不需要请求)。
Note that index.ts appears in italics in the file list on the left. This indicates that it isn’t a “real” file in the sense that the web page included it. Rather, it was included via the source map. Depending on your settings, index.js.map will contain either a reference to index.ts (in which case the browser loads it over the network) or an inline copy of it (in which case no request is needed).
使用 source maps 需要注意以下几点:
There are a few things to be aware of with source maps:
如果您在 TypeScript 中使用捆绑器或缩小器,它可能会生成自己的源映射。为了获得最佳调试体验,您希望它一直映射回原始 TypeScript 源,而不是生成的 JavaScript。如果您的捆绑器内置了对 TypeScript 的支持,那么这应该就可以了。如果没有,您可能需要寻找一些标志以使其读取源映射输入。
If you are using a bundler or minifier with TypeScript, it may generate a source map of its own. To get the best debugging experience, you want this to map all the way back to the original TypeScript sources, not the generated JavaScript. If your bundler has built-in support for TypeScript, then this should just work. If not, you may need to hunt down some flags to make it read source map inputs.
请注意您是否在生产中提供源映射。除非调试器打开,否则浏览器不会加载源映射,因此对最终用户没有性能影响。但是,如果源映射包含原始源代码的内联副本,则可能存在您不打算公开的内容。全世界真的需要看到你尖刻的评论或内部错误跟踪器 URL 吗?
Be aware of whether you’re serving source maps in production. The browser won’t load source maps unless the debugger is open, so there’s no performance impact for end users. But if the source map contains an inline copy of your original source code, then there may be content that you didn’t intend to publicize. Does the world really need to see your snarky comments or internal bug tracker URLs?
您还可以使用源映射调试 NodeJS 程序。这通常是通过您的编辑器或通过从浏览器的调试器连接到您的节点进程来完成的。有关详细信息,请参阅 Node 文档。
You can also debug NodeJS programs using source maps. This is typically done via your editor or by connecting to your node process from a browser’s debugger. Consult the Node docs for details.
类型检查器可以在您运行代码之前捕获许多错误,但它不能替代一个好的调试器。使用源映射获得出色的 TypeScript 调试体验。
The type checker can catch many errors before you run your code, but it is no substitute for a good debugger. Use source maps to get a great TypeScript debugging experience.
不要调试生成的 JavaScript。使用源映射在运行时调试 TypeScript 代码。
Don’t debug generated JavaScript. Use source maps to debug your TypeScript code at runtime.
确保您的源映射一直映射到您运行的代码。
Make sure that your source maps are mapped all the way through to the code that you run.
Depending on your settings, your source maps might contain an inline copy of your original code. Don’t publish them unless you know what you’re doing!
你有听说 TypeScript 很棒。您还从痛苦的经历中了解到,维护您已有 15 年历史的 100,000 行 JavaScript 库并非如此。如果它能成为一个 TypeScript 库就好了!
You’ve heard that TypeScript is great. You also know from painful experience that maintaining your 15-year-old, 100,000-line JavaScript library isn’t. If only it could become a TypeScript library!
本章提供了一些关于将 JavaScript 项目迁移到 TypeScript 而不会失去理智和放弃努力的建议。
This chapter offers some advice about migrating your JavaScript project to TypeScript without losing your sanity and abandoning the effort.
只能一次性迁移最小的代码库。大型项目的关键是逐步迁移。条目 60讨论了如何做到这一点。对于长期迁移,跟踪您的进度并确保您不会倒退是必不可少的。这为变革创造了一种势头和必然性的感觉。条目 61讨论了如何做到这一点。
Only the smallest codebases can be migrated in one fell swoop. The key for larger projects is to migrate gradually. Item 60 discusses how to do this. For a long migration, it’s essential to track your progress and make sure you don’t backslide. This creates a sense of momentum and inevitability to the change. Item 61 discusses ways to do this.
将大型项目迁移到 TypeScript 不一定容易,但它确实提供了巨大的潜在优势。2017 年的一项研究发现,在 GitHub 上的 JavaScript 项目中修复的错误中有 15% 可以通过 TypeScript 避免。1更令人印象深刻的是,一项针对 AirBnb 六个月的事后分析的调查发现,其中 38% 的分析本可以通过 TypeScript 避免。2如果您在您的组织中提倡使用 TypeScript,这些统计数据将会有所帮助!进行一些实验和寻找早期采用者也是如此。Item 59讨论了如何在开始迁移之前尝试使用 TypeScript。
Migrating a large project to TypeScript won’t necessarily be easy, but it does offer a huge potential upside. A 2017 study found that 15% of bugs fixed in JavaScript projects on GitHub could have been prevented with TypeScript.1 Even more impressive, a survey of six months’ worth of postmortems at AirBnb found that 38% of them could have been prevented by TypeScript.2 If you’re advocating for TypeScript at your organization, stats like these will help! So will running some experiments and finding early adopters. Item 59 discusses how to experiment with TypeScript before you begin migration.
由于本章主要是关于 JavaScript,因此许多代码示例要么是纯 JavaScript(并且预计不会通过类型检查器),要么使用较宽松的设置(例如,关闭)进行检查noImplicitAny。
Since this chapter is largely about JavaScript, many of the code samples are either pure JavaScript (and not expected to pass the type checker) or checked with looser settings (e.g., with noImplicitAny off).
在除了检查代码的类型安全之外,TypeScript 还将您的 TypeScript 代码编译为任何版本的 JavaScript 代码,一直追溯到 1999 年的老式 ES3。由于 TypeScript 是最新版本 JavaScript 的超集,这意味着您可以将其用作tsc“转译器”:它采用新的 JavaScript 并将其转换为旧的、更广泛支持的 JavaScript。
In addition to checking your code for type safety, TypeScript compiles your TypeScript code to any version of JavaScript code, all the way back to 1999 vintage ES3. Since TypeScript is a superset of the latest version of JavaScript, this means that you can use tsc as a “transpiler”: something that takes new JavaScript and converts it to older, more widely supported JavaScript.
从不同的角度来看,这意味着当您决定将现有的 JavaScript 代码库转换为 TypeScript 时,采用所有最新的 JavaScript 功能没有任何不利之处。事实上,它有很多好处:因为 TypeScript 是为与现代 JavaScript 一起工作而设计的,所以现代化你的 JS 是采用 TypeScript 的重要的第一步。
Taking a different perspective, this means that when you decide to convert an existing JavaScript codebase to TypeScript, there’s no downside to adopting all the latest JavaScript features. In fact, there’s quite a bit of upside: because TypeScript is designed to work with modern JavaScript, modernizing your JS is a great first step toward adopting TypeScript.
而且由于 TypeScript 是 JavaScript 的超集,学习编写更现代和惯用的 JavaScript 意味着您也在学习编写更好的 TypeScript。
And because TypeScript is a superset of JavaScript, learning to write more modern and idiomatic JavaScript means you’re learning to write better TypeScript, too.
此项快速浏览了现代 JavaScript 中的一些功能,我在这里将其定义为 ES2015(又名 ES6)及之后引入的所有功能。该材料在其他书籍和网上有更详细的介绍。如果这里提到的任何主题不熟悉,您应该自己多了解一下。async当您学习/等新语言功能时,TypeScript 会非常有用await:它几乎肯定比您更了解该功能,并且可以指导您正确使用。
This item gives a quick tour of some of the features in modern JavaScript, which I’m defining here as everything introduced in ES2015 (aka ES6) and after. This material is covered in much greater detail in other books and online. If any of the topics mentioned here are unfamiliar, you owe it to yourself to learn more about them. TypeScript can be tremendously helpful when you’re learning a new language feature like async/await: it almost certainly understands the feature better than you do and can guide you toward correct usage.
这些都值得理解,但到目前为止,采用 TypeScript 最重要的是 ECMAScript 模块和 ES2015 类。
These are all worth understanding, but by far the most important for adopting TypeScript are ECMAScript Modules and ES2015 classes.
前2015 版的 ECMAScript 没有标准的方法来将你的代码分解成单独的模块。有很多解决方案,从多个<script>标签、手动连接和 Makefiles 到 node.js 风格的require语句或 AMD 风格的define回调。TypeScript 甚至有自己的模块系统(条目 53)。
Before the 2015 version of ECMAScript there was no standard way to break your code into separate modules. There were many solutions, from multiple <script> tags, manual concatenation, and Makefiles to node.js-style require statements or AMD-style define callbacks. TypeScript even had its own module system (Item 53).
今天只有一个标准:ECMAScript 模块,import又名export. 如果你的 JavaScript 代码库仍然是单个文件,如果你使用连接或其他模块系统之一,那么是时候切换到 ES 模块了。这可能需要设置一个工具,例如webpack 或 ts-node。TypeScript 最适合与 ES 模块一起使用,采用它们将有助于您的转换,尤其是因为它允许您一次迁移一个模块(参见条目 61)。
Today there is one standard: ECMAScript modules, aka import and export. If your JavaScript codebase is still a single file, if you use concatenation or one of the other module systems, it’s time to switch to ES modules. This may require setting up a tool like webpack or ts-node. TypeScript works best with ES modules, and adopting them will facilitate your transition, not least because it will allow you to migrate modules one at a time (see Item 61).
详细信息将根据您的设置而有所不同,但如果您像这样使用 CommonJS:
The details will vary depending on your setup, but if you’re using CommonJS like this:
// CommonJS// a.jsconstb=require('./b');console.log(b.name);// b.jsconstname='Module B';module.exports={name};
// CommonJS// a.jsconstb=require('./b');console.log(b.name);// b.jsconstname='Module B';module.exports={name};
那么等效的 ES 模块如下所示:
then the ES module equivalent would look like:
// ECMAScript module// a.tsimport*asbfrom'./b';console.log(b.name);// b.tsexportconstname='Module B';
// ECMAScript module// a.tsimport*asbfrom'./b';console.log(b.name);// b.tsexportconstname='Module B';
JavaScript有一个灵活的基于原型的对象模型。但总的来说,JS 开发人员忽略了这一点,转而支持更严格的基于类的模型。这在介绍中被正式写入语言中classES2015 中的关键字。
JavaScript has a flexible prototype-based object model. But by and large JS developers have ignored this in favor of a more rigid class-based model. This was officially enshrined into the language with the introduction of the class keyword in ES2015.
如果您的代码以直接的方式使用原型,切换到使用类。也就是说,而不是:
If your code uses prototypes in a straightforward way, switch to using classes. That is, instead of:
functionPerson(first,last){this.first=first;this.last=last;}Person.prototype.getName=function(){returnthis.first+' '+this.last;}constmarie=newPerson('Marie','Curie');constpersonName=marie.getName();
functionPerson(first,last){this.first=first;this.last=last;}Person.prototype.getName=function(){returnthis.first+' '+this.last;}constmarie=newPerson('Marie','Curie');constpersonName=marie.getName();
写:
write:
classPerson{first:string;last:string;constructor(first:string,last:string){this.first=first;this.last=last;}getName() {returnthis.first+' '+this.last;}}constmarie=newPerson('Marie','Curie');constpersonName=marie.getName();
classPerson{first:string;last:string;constructor(first:string,last:string){this.first=first;this.last=last;}getName() {returnthis.first+' '+this.last;}}constmarie=newPerson('Marie','Curie');constpersonName=marie.getName();
TypeScript 在 的原型版本上苦苦挣扎,Person但可以理解带有最少注释的基于类的版本。如果您不熟悉语法,TypeScript 将帮助您正确使用它。
TypeScript struggles with the prototype version of Person but understands the class-based version with minimal annotations. If you’re unfamiliar with the syntax, TypeScript will help you get it right.
为了对于使用旧式类的代码,TypeScript 语言服务提供了“将函数转换为 ES2015 类”的快速修复,可以加快此过程(图 8-1)。
For code that uses older-style classes, the TypeScript language service offers a “Convert function to an ES2015 class” quick fix that can speed this up (Figure 8-1).
JavaScript的 var有一些著名的古怪范围规则。如果您想了解更多关于它们的信息,请阅读Effective JavaScript。但最好避免var,不要担心!相反,使用let和const。它们是真正的块范围的,并且以比var.
JavaScript’s var has some famously quirky scoping rules. If you’re curious to learn more about them, read Effective JavaScript. But better to avoid var and not worry! Instead, use let and const. They’re truly block-scoped and work in much more intuitive ways than var.
同样,TypeScript 将在这里为您提供帮助。如果更改var为let导致错误,那么您几乎可以肯定正在做不应该做的事情。
Again, TypeScript will help you here. If changing var to let results in an error, then you’re almost certainly doing something you shouldn’t be.
嵌套函数语句也有var-like 作用域规则:
Nested function statements also have var-like scoping rules:
functionfoo() {bar();functionbar() {console.log('hello');}}
functionfoo() {bar();functionbar() {console.log('hello');}}
当您调用 时foo(),它会记录下来hello,因为 的定义bar被提升到 的顶部foo。这太令人惊讶了!更喜欢函数表达式 ( const bar = () => { ... }) 而不是。
When you call foo(), it logs hello because the definition of bar is hoisted to the top of foo. This is surprising! Prefer function expressions (const bar = () => { ... }) instead.
在经典 JavaScript,您使用 C 风格的 for 循环来遍历数组:
In classic JavaScript you used a C-style for loop to iterate over an array:
for(vari=0;i<array.length;i++){constel=array[i];// ...}
for(vari=0;i<array.length;i++){constel=array[i];// ...}
在现代 JavaScript 中,您可以改用 for-of 循环:
In modern JavaScript you can use a for-of loop instead:
for(constelofarray){// ...}
for(constelofarray){// ...}
这不太容易出现拼写错误,并且不会引入索引变量。如果你想要索引变量,你可以使用forEach:
This is less prone to typos and doesn’t introduce an index variable. If you want the index variable, you can use forEach:
array.forEach((el,i)=>{// ...});
array.forEach((el,i)=>{// ...});
避免使用 for-in 结构来遍历数组,因为它有很多意外(见条款 16)。
Avoid using the for-in construct to iterate over arrays as it has many surprises (see Item 16).
这 this关键字是 JavaScript 最著名的混淆方面之一,因为它与其他变量具有不同的范围规则:
The this keyword is one of the most famously confusing aspects of JavaScript because it has different scoping rules than other variables:
classFoo{method() {console.log(this);[1,2].forEach(function(i){console.log(this);});}}constf=newFoo();f.method();// Prints Foo, undefined, undefined in strict mode// Prints Foo, window, window (!) in non-strict mode
classFoo{method() {console.log(this);[1,2].forEach(function(i){console.log(this);});}}constf=newFoo();f.method();// Prints Foo, undefined, undefined in strict mode// Prints Foo, window, window (!) in non-strict mode
通常你想this引用你所在类的相关实例。箭头函数通过将值保留this在它们的封闭范围内来帮助你做到这一点:
Generally you want this to refer to the relevant instance of whichever class you’re in. Arrow functions help you do that by keeping the this value from their enclosing scope:
classFoo{method() {console.log(this);[1,2].forEach(i=>{console.log(this);});}}constf=newFoo();f.method();// Always prints Foo, Foo, Foo
classFoo{method() {console.log(this);[1,2].forEach(i=>{console.log(this);});}}constf=newFoo();f.method();// Always prints Foo, Foo, Foo
除了具有更简单的语义外,箭头函数更简洁。您应该尽可能使用它们。有关this绑定的更多信息,请参阅条目 49。和noImplicitThis(或strict)编译器选项,TypeScript 将帮助您获得this-binding 权利。
In addition to having simpler semantics, arrow functions are more concise. You should use them whenever possible. For more on this binding, see Item 49. With the noImplicitThis (or strict) compiler option, TypeScript will help you get your this-binding right.
constx=1,y=2,z=3;constpt={x:x,y:y,z:z};
constx=1,y=2,z=3;constpt={x:x,y:y,z:z};
你可以简单地写:
you can simply write:
constx=1,y=2,z=3;constpt={x,y,z};
constx=1,y=2,z=3;constpt={x,y,z};
除了更简洁之外,这还鼓励变量和属性的一致命名,您的人类读者也会欣赏这一点(条目 36)。
In addition to being more concise, this encourages consistent naming of variables and properties, something your human readers will appreciate as well (Item 36).
要从箭头函数返回对象字面量,请将其括在括号中:
To return an object literal from an arrow function, wrap it in parentheses:
['A','B','C'].map((char,idx)=>({char,idx}));// [ { char: 'A', idx: 0 }, { char: 'B', idx: 1 }, { char: 'C', idx: 2 } ]
['A','B','C'].map((char,idx)=>({char,idx}));// [ { char: 'A', idx: 0 }, { char: 'B', idx: 1 }, { char: 'C', idx: 2 } ]
对于值为函数的属性也有简写:
There is also shorthand for properties whose values are functions:
constobj={onClickLong:function(e){// ...},onClickCompact(e){// ...}};
constobj={onClickLong:function(e){// ...},onClickCompact(e){// ...}};
紧凑对象字面量的反面是对象解构。而不是写:
The inverse of compact object literals is object destructuring. Instead of writing:
constprops=obj.props;consta=props.a;constb=props.b;
constprops=obj.props;consta=props.a;constb=props.b;
你可以写:
you can write:
const{props}=obj;const{a,b}=props;
const{props}=obj;const{a,b}=props;
甚至:
or even:
const{props:{a,b}}=obj;
const{props:{a,b}}=obj;
仅在最后一个示例中a, 和b成为变量,而不是props.
In this last example only a and b become variables, not props.
您可以在解构时指定默认值。而不是写:
You may specify default values when destructuring. Instead of writing:
let{a}=obj.props;if(a===undefined)a='default';
let{a}=obj.props;if(a===undefined)a='default';
写这个:
write this:
const{a='default'}=obj.props;
const{a='default'}=obj.props;
您还可以解构数组。这对于元组类型特别有用:
You can also destructure arrays. This is particularly useful with tuple types:
constpoint=[1,2,3];const[x,y,z]=point;const[,a,b]=point;// Ignore the first one
constpoint=[1,2,3];const[x,y,z]=point;const[,a,b]=point;// Ignore the first one
解构也可以用在函数参数中:
Destructuring can also be used in function parameters:
constpoints=[[1,2,3],[4,5,6],];points.forEach(([x,y,z])=>console.log(x+y+z));// Logs 6, 15
constpoints=[[1,2,3],[4,5,6],];points.forEach(([x,y,z])=>console.log(x+y+z));// Logs 6, 15
与紧凑的对象文字语法一样,解构是简洁的并且鼓励一致的变量命名。用它!
As with compact object literal syntax, destructuring is concise and encourages consistent variable naming. Use it!
In JavaScript, all function parameters are optional:
functionlog2(a,b){console.log(a,b);}log2();
functionlog2(a,b){console.log(a,b);}log2();
这输出:
This outputs:
未定义
undefined undefined
这通常用于实现参数的默认值:
This is often used to implement default values for parameters:
functionparseNum(str,base){base=base||10;returnparseInt(str,base);}
functionparseNum(str,base){base=base||10;returnparseInt(str,base);}
在现代 JavaScript 中,您可以直接在参数列表中指定默认值:
In modern JavaScript, you can specify the default value directly in the parameter list:
functionparseNum(str,base=10){returnparseInt(str,base);}
functionparseNum(str,base=10){returnparseInt(str,base);}
除了更简洁之外,这还清楚地表明这base是一个可选参数。当您迁移到 TypeScript 时,默认参数还有另一个好处:它们帮助类型检查器推断参数的类型,从而消除了对类型注释的需要。请参阅第 19 项。
In addition to being more concise, this makes it clear that base is an optional parameter. Default parameters have another benefit when you migrate to TypeScript: they help the type checker infer the type of the parameter, removing the need for a type annotation. See Item 19.
第 25 项说明为什么async和await更可取,但要点是它们将简化您的代码、防止错误并帮助类型在您的异步代码中流动。
Item 25 explains why async and await are preferable, but the gist is that they’ll simplify your code, prevent bugs, and help types flow through your asynchronous code.
而不是以下任何一个:
Instead of either of these:
functiongetJSON(url:string){returnfetch(url).then(response=>response.json());}functiongetJSONCallback(url:string,cb:(result:unknown)=>void){// ...}
functiongetJSON(url:string){returnfetch(url).then(response=>response.json());}functiongetJSONCallback(url:string,cb:(result:unknown)=>void){// ...}
写这个:
write this:
asyncfunctiongetJSON(url:string){constresponse=awaitfetch(url);returnresponse.json();}
asyncfunctiongetJSON(url:string){constresponse=awaitfetch(url);returnresponse.json();}
ES5引入了“严格模式”,使一些可疑的模式更明显地出错。'use strict'您可以通过输入代码来启用它:
ES5 introduced “strict mode” to make some suspect patterns more explicit errors. You enable it by putting 'use strict' in your code:
'use strict';functionfoo(){x=10;// Throws in strict mode, defines a global in non-strict.}
'use strict';functionfoo(){x=10;// Throws in strict mode, defines a global in non-strict.}
如果您从未在 JavaScript 代码库中使用过严格模式,请尝试一下。它发现的错误很可能也是 TypeScript 编译器会发现的错误。
If you’ve never used strict mode in your JavaScript codebase, then give it a try. The errors it finds are likely to be ones that the TypeScript compiler will find, too.
'use strict'但是,当您过渡到 TypeScript 时,保留源代码没有太大价值。总的来说,TypeScript 提供的健全性检查远比严格模式提供的检查严格。
But as you transition to TypeScript, there’s not much value in keeping 'use strict' in your source code. By and large, the sanity checks that TypeScript provides are far stricter than those offered by strict mode.
那里'use strict'在发出的JavaScript 中使用 a 是有一定价值的tsc。如果您设置alwaysStrict或strict编译器选项,TypeScript 将以严格模式解析您的代码,并'use strict'为您将 a 放入 JavaScript 输出中。
There is some value in having a 'use strict' in the JavaScript that tsc emits. If you set the alwaysStrict or strict compiler options, TypeScript will parse your code in strict mode and put a 'use strict' in the JavaScript output for you.
简而言之,不要'use strict'用 TypeScript 编写。改用alwaysStrict。
In short, don’t write 'use strict' in your TypeScript. Use alwaysStrict instead.
这些只是 TypeScript 允许您使用的众多新 JavaScript 功能中的一小部分。管理 JS 标准的机构 TC39 非常活跃,并且每年都会添加新功能。TypeScript 团队目前致力于实现标准化过程中达到第 3 阶段(共 4 个阶段)的任何功能,因此您甚至不必等待墨水变干。查看 TC39 GitHub 存储库3了解最新信息。在撰写本文时,管道和装饰器提案尤其具有影响 TypeScript 的巨大潜力。
These are just a few of the many new JavaScript features that TypeScript lets you use. TC39, the body that governs JS standards, is very active, and new features are added year to year. The TypeScript team is currently committed to implementing any feature that reaches stage 3 (out of 4) in the standardization process, so you don’t even have to wait for the ink to dry. Check out the TC39 GitHub repo3 for the latest. As of this writing, the pipeline and decorators proposals in particular have great potential to impact TypeScript.
TypeScript 让您可以在任何运行时环境中编写现代 JavaScript。通过使用它启用的语言功能来利用这一点。除了改进您的代码库之外,这还将帮助 TypeScript 理解您的代码。
TypeScript lets you write modern JavaScript whatever your runtime environment. Take advantage of this by using the language features it enables. In addition to improving your codebase, this will help TypeScript understand your code.
使用 TypeScript 学习类、解构和async/ 等语言特性await。
Use TypeScript to learn language features like classes, destructuring, and async/await.
不要费心'use strict':TypeScript 更严格。
Don’t bother with 'use strict': TypeScript is stricter.
Check the TC39 GitHub repo and TypeScript release notes to learn about all the latest language features.
前如果您开始将源文件从 JavaScript 转换为 TypeScript(条目 60),您可能想要尝试类型检查以初步了解将出现的各种问题。TypeScript 的@ts-check指令可以让你做到这一点。它指示类型检查器分析单个文件并报告它发现的任何问题。您可以将其视为类型检查的极其宽松的版本:甚至比noImplicitAny关闭 TypeScript 还要宽松(第 2 项)。
Before you begin the process of converting your source files from JavaScript to TypeScript (Item 60), you may want to experiment with type checking to get an initial read on the sorts of issues that will come up. TypeScript’s @ts-check directive lets you do exactly this. It directs the type checker to analyze a single file and report whatever issues it finds. You can think of it as an extremely loose version of type checking: looser even than TypeScript with noImplicitAny off (Item 2).
它是这样工作的:
Here’s how it works:
// @ts-checkconstperson={first:'Grace',last:'Hopper'};2*person.first// ~~~~~~~~~~~~ The right-hand side of an arithmetic operation must be of type// 'any', 'number', 'bigint', or an enum type
// @ts-checkconstperson={first:'Grace',last:'Hopper'};2*person.first// ~~~~~~~~~~~~ The right-hand side of an arithmetic operation must be of type// 'any', 'number', 'bigint', or an enum type
person.firstTypeScript 推断as的类型string,因此2 * person.first是类型错误,不需要类型注释。
TypeScript infers the type of person.first as string, so 2 * person.first is a type error, no type annotations required.
虽然它可能会出现这种明显的类型错误,或者使用太多参数调用的函数,但在实践中,// @ts-check往往会出现一些特定类型的错误:
While it may surface this sort of blatant type error, or functions called with too many arguments, in practice, // @ts-check tends to turn up a few specific types of errors:
如果这些是您要定义的符号,然后用let或声明它们const。如果它们是在别处定义的“环境”符号(<script>例如,在 HTML 文件的标记中),那么您可以创建一个类型声明文件来描述它们。
If these are symbols that you’re defining, then declare them with let or const. If they are “ambient” symbols that are defined elsewhere (in a <script> tag in an HTML file, for instance), then you can create a type declarations file to describe them.
例如,如果你有这样的 JavaScript:
For example, if you have JavaScript like this:
// @ts-checkconsole.log(user.firstName);// ~~~~ Cannot find name 'user'
// @ts-checkconsole.log(user.firstName);// ~~~~ Cannot find name 'user'
然后你可以创建一个名为types.d.ts的文件:
then you could create a file called types.d.ts:
interfaceUserData{firstName:string;lastName:string;}declareletuser:UserData;
interfaceUserData{firstName:string;lastName:string;}declareletuser:UserData;
自行创建此文件可能会解决此问题。如果没有,您可能需要使用“三斜杠”引用显式导入它:
Creating this file on its own may fix the issue. If it does not, you may need to explicitly import it with a “triple-slash” reference:
// @ts-check/// <reference path="./types.d.ts" />console.log(user.firstName);// OK
// @ts-check/// <reference path="./types.d.ts" />console.log(user.firstName);// OK
这个types.d.ts文件是一个有价值的工件,它将成为您项目类型声明的基础。
This types.d.ts file is a valuable artifact that will become the basis for your project’s type declarations.
如果你正在使用第三方库,TypeScript 需要知道它。例如,您可以使用 jQuery 来设置 HTML 元素的大小。使用@ts-check,TypeScript 将标记错误:
If you’re using a third-party library, TypeScript needs to know about it. For example, you might use jQuery to set the size of an HTML element. With @ts-check, TypeScript will flag an error:
// @ts-check$('#graph').style({'width':'100px','height':'100px'});// ~ Cannot find name '$'
// @ts-check$('#graph').style({'width':'100px','height':'100px'});// ~ Cannot find name '$'
The solution is to install the type declarations for jQuery:
$ npm install --save-dev @types/jquery
$ npm install --save-dev @types/jquery
现在错误是特定于 jQuery 的:
Now the error is specific to jQuery:
// @ts-check$('#graph').style({'width':'100px','height':'100px'});// ~~~~~ Property 'style' does not exist on type 'JQuery<HTMLElement>'
// @ts-check$('#graph').style({'width':'100px','height':'100px'});// ~~~~~ Property 'style' does not exist on type 'JQuery<HTMLElement>'
其实应该是.css,不是.style。
In fact, it should be .css, not .style.
@ts-check让您可以利用流行的 JavaScript 库的 TypeScript 声明,而无需自己迁移到 TypeScript。这是使用它的最佳理由之一。
@ts-check lets you take advantage of the TypeScript declarations for popular JavaScript libraries without migrating to TypeScript yourself. This is one of the best reasons to use it.
假设你正在编写在网络浏览器中运行的代码,TypeScript 可能会标记你处理 DOM 元素的问题。例如:
Assuming you’re writing code that runs in a web browser, TypeScript is likely to flag issues around your handling of DOM elements. For example:
// @ts-checkconstageEl=document.getElementById('age');ageEl.value='12';// ~~~~~ Property 'value' does not exist on type 'HTMLElement'
// @ts-checkconstageEl=document.getElementById('age');ageEl.value='12';// ~~~~~ Property 'value' does not exist on type 'HTMLElement'
问题是只有HTMLInputElements 有一个value属性,但document.getElementById返回更通用的属性HTMLElement(参见条目 55)。如果您知道该#age元素确实是一个input元素,那么这是使用类型断言的合适时机(条目 9)。但这仍然是一个 JS 文件,所以你不能写as HTMLInputElement. 相反,您可以使用 JSDoc 声明一个类型:
The issue is that only HTMLInputElements have a value property, but document.getElementById returns the more generic HTMLElement (see Item 55). If you know that the #age element really is an input element, then this is an appropriate time to use a type assertion (Item 9). But this is still a JS file, so you can’t write as HTMLInputElement. Instead, you can assert a type using JSDoc:
// @ts-checkconstageEl=/** @type {HTMLInputElement} */(document.getElementById('age'));ageEl.value='12';// OK
// @ts-checkconstageEl=/** @type {HTMLInputElement} */(document.getElementById('age'));ageEl.value='12';// OK
如果将鼠标悬停ageEl在编辑器中,您会看到 TypeScript 现在将其视为一个HTMLInputElement. 键入 JSDoc@type注释时要小心:注释后的括号是必需的。
If you mouse over ageEl in your editor, you’ll see that TypeScript now considers it an HTMLInputElement. Take care as you type the JSDoc @type annotation: the parentheses after the comment are required.
这会导致另一种类型的错误@ts-check,不准确的 JSDoc,如下所述。
This leads to another type of error that comes up with @ts-check, inaccurate JSDoc, as explained next.
如果你的项目已经有 JSDoc 风格的注释,TypeScript 会在你打开@ts-check. 如果您以前使用过类似的系统使用这些注释来强制类型安全的 Closure 编译器,那么这不应该引起大问题。但是,如果您的评论更像是“有抱负的 JSDoc”,您可能会感到惊讶:
If your project already has JSDoc-style comments, TypeScript will begin checking them when you flip on @ts-check. If you previously used a system like the Closure Compiler that used these comments to enforce type safety, then this shouldn’t cause major headaches. But you may be in for some surprises if your comments were more like “aspirational JSDoc”:
// @ts-check/*** Gets the size (in pixels) of an element.* @param {Node} el The element* @return {{w: number, h: number}} The size*/functiongetSize(el){constbounds=el.getBoundingClientRect();// ~~~~~~~~~~~~~~~~~~~~~ Property 'getBoundingClientRect'// does not exist on type 'Node'return{width:bounds.width,height:bounds.height};// ~~~~~~~~~~~~~~~~~~~ Type '{ width: any; height: any; }' is not// assignable to type '{ w: number; h: number; }'}
// @ts-check/*** Gets the size (in pixels) of an element.* @param {Node} el The element* @return {{w: number, h: number}} The size*/functiongetSize(el){constbounds=el.getBoundingClientRect();// ~~~~~~~~~~~~~~~~~~~~~ Property 'getBoundingClientRect'// does not exist on type 'Node'return{width:bounds.width,height:bounds.height};// ~~~~~~~~~~~~~~~~~~~ Type '{ width: any; height: any; }' is not// assignable to type '{ w: number; h: number; }'}
第一个问题是对 DOM 的误解:getBoundingClientRect()定义在Element,而不是Node。所以@param应该更新标签。@return第二个是标记中指定的属性与实现之间的不匹配。大概项目的其余部分使用了width和height属性,因此@return应该更新标签。
The first issue is a misunderstanding of the DOM: getBoundingClientRect() is defined on Element, not Node. So the @param tag should be updated. The second is a mismatch between proprties specified in the @return tag and the implementation. Presumably the rest of the project uses the width and height properties, so the @return tag should be updated.
您可以使用 JSDoc 逐步将类型注释添加到您的项目中。TypeScript 语言服务将提供推断类型注释,作为对使用明确的代码的快速修复,如此处和图 8-2所示:
You can use JSDoc to gradually add type annotations to your project. The TypeScript language service will offer to infer type annotations as a quick fix for code where it’s clear from usage, as shown here and in Figure 8-2:
functiondouble(val){return2*val;}
functiondouble(val){return2*val;}
这会产生正确的 JSDoc 注释:
This results in a correct JSDoc annotation:
// @ts-check/*** @param {number} val*/functiondouble(val){return2*val;}
// @ts-check/*** @param {number} val*/functiondouble(val){return2*val;}
这有助于鼓励类型使用@ts-check. 但它并不总是那么好用。例如:
This can be helpful to encourage types to flow through your code with @ts-check. But it doesn’t always work so well. For instance:
functionloadData(data){data.files.forEach(asyncfile=>{// ...});}
functionloadData(data){data.files.forEach(asyncfile=>{// ...});}
如果您使用快速修复来注释data,您将得到:
If you use the quick fix to annotate data, you’ll wind up with:
/*** @param {{* files: { forEach: (arg0: (file: any) => Promise<void>) => void; };* }} data*/functionloadData(data){// ...}
/*** @param {{* files: { forEach: (arg0: (file: any) => Promise<void>) => void; };* }} data*/functionloadData(data){// ...}
这是结构类型出错了(第 4 项)。虽然该函数在技术上适用于具有forEach该签名方法的任何类型的对象,但其意图很可能是参数为{files: string[]}.
This is structural typing gone awry (Item 4). While the function would technically work on any sort of object with a forEach method with that signature, the intent was most likely for the parameter to be {files: string[]}.
您可以使用 JSDoc 注释和@ts-check. 这很有吸引力,因为它不需要更改您的工具。但最好不要朝这个方向走得太远。注释样板文件具有实际成本:您的逻辑很容易迷失在 JSDoc 的海洋中。TypeScript 最适合.ts文件,而不是.js文件。最终目标是将您的项目转换为 TypeScript,而不是带有 JSDoc 注释的 JavaScript。但@ts-check可以作为一种有用的方法来试验类型并发现一些初始错误,特别是对于已经具有大量 JSDoc 注释的项目。
You can get much of the TypeScript experience in a JavaScript project using JSDoc annotations and @ts-check. This is appealing because it requires no changes in your tooling. But it’s best not to go too far in this direction. Comment boilerplate has real costs: it’s easy for your logic to get lost in a sea of JSDoc. TypeScript works best with .ts files, not .js files. The goal is ultimately to convert your project to TypeScript, not to JavaScript with JSDoc annotations. But @ts-check can be a useful way to experiment with types and find some initial errors, especially for projects that already have extensive JSDoc annotations.
将 " // @ts-check" 添加到 JavaScript 文件的顶部以启用类型检查。
Add "// @ts-check" to the top of a JavaScript file to enable type checking.
识别常见错误。了解如何声明全局变量并为第三方库添加类型声明。
Recognize common errors. Know how to declare globals and add type declarations for third-party libraries.
使用 JSDoc 注释进行类型断言和更好的类型推断。
Use JSDoc annotations for type assertions and better type inference.
Don’t spend too much time getting your code perfectly typed with JSDoc. Remember that the goal is to convert to .ts!
为了一个小项目,你可以一口气从 JavaScript 转换为 TypeScript。但是对于更大的项目,这种“停止世界”的方法是行不通的。您需要能够逐渐过渡。这意味着您需要一种让 TypeScript 和 JavaScript 共存的方法。
For a small project, you may be able to convert from JavaScript to TypeScript in one fell swoop. But for a larger project this “stop the world” approach won’t work. You need to be able to transition gradually. That means you need a way for TypeScript and JavaScript to coexist.
这关键是allowJs编译器选项。使用allowJs,TypeScript 文件和 JavaScript 文件可以相互导入。对于 JavaScript 文件,此模式非常宽松。除非您使用@ts-check(Item 59),否则您将看到的唯一错误是语法错误。这是最简单意义上的“TypeScript 是 JavaScript 的超集”。
The key to this is the allowJs compiler option. With allowJs, TypeScript files and JavaScript files may import one another. For JavaScript files this mode is extremely permissive. Unless you use @ts-check (Item 59), the only errors you’ll see are syntax errors. This is “TypeScript is a superset of JavaScript” in the most trivial sense.
虽然它不太可能发现错误,allowJs但确实让您有机会在开始更改代码之前将 TypeScript 引入您的构建链。这是个好主意,因为您希望能够在将模块转换为 TypeScript 时运行测试(条目 61)。
While it’s unlikely to catch errors, allowJs does give you an opportunity to introduce TypeScript into your build chain before you start making code changes. This is a good idea because you’ll want to be able to run your tests as you convert modules to TypeScript (Item 61).
如果您的捆绑器包含 TypeScript 集成或有可用的插件,那通常是最简单的方法。和 browserify,例如,您运行npm install --sav-dev tsify并将其添加为插件:
If your bundler includes TypeScript integration or has a plug-in available, that’s usually the easiest path forward. With browserify, for instance, you run npm install --sav-dev tsify and add it as a plug-in:
$ browserify index.ts -p [ tsify --noImplicitAny ] > bundle.js
$ browserify index.ts -p [ tsify --noImplicitAny ] > bundle.js
大多数单元测试工具也有这样的选项。jest例如,使用该工具,您可以ts-jest通过指定jest.config.js类似的方式安装和传递 TypeScript 源代码:
Most unit testing tools have an option like this as well. With the jest tool, for instance, you install ts-jest and pass TypeScript sources through it by specifying a jest.config.js like:
module.exports={transform:{'^.+\\.tsx?$':'ts-jest',},};
module.exports={transform:{'^.+\\.tsx?$':'ts-jest',},};
如果您的构建链是自定义的,您的任务将更加复杂。但总有一个很好的后备选项:当您指定该outDir选项时,TypeScript 将在与您的源代码树平行的目录中生成纯 JavaScript 源代码。通常你现有的构建链可以在上面运行。您可能需要调整 TypeScript 的 JavaScript 输出,使其与您的原始 JavaScript 源代码紧密匹配(例如,通过指定target和module选项)。
If your build chain is custom, your task will be more involved. But there’s always a good fallback option: when you specify the outDir option, TypeScript will generate pure JavaScript sources in a directory that parallels your source tree. Usually your existing build chain can be run over that. You may need to tweak TypeScript’s JavaScript output so that it closely matches your original JavaScript source, (e.g., by specifying the target and module options).
将 TypeScript 添加到您的构建和测试过程中可能不是最令人愉快的任务,但它是一项必不可少的任务,可以让您自信地开始迁移代码。
Adding TypeScript into your build and test process may not be the most enjoyable task, but it is an essential one that will let you begin to migrate your code with confidence.
在您转换项目时,使用allowJs编译器选项支持混合的 JavaScript 和 TypeScript。
Use the allowJs compiler option to support mixed JavaScript and TypeScript as you transition your project.
在开始大规模迁移之前,让您的测试和构建链使用 TypeScript。
Get your tests and build chain working with TypeScript before beginning large-scale migration.
你有采用现代 JavaScript,将您的项目转换为使用 ECMAScript 模块和类(条目 58)。您已将 TypeScript 集成到您的构建链中,并通过了所有测试(项目 60)。现在是有趣的部分:将 JavaScript 转换为 TypeScript。但是从哪里开始呢?
You’ve adopted modern JavaScript, converting your project to use ECMAScript modules and classes (Item 58). You’ve integrated TypeScript into your build chain and have all your tests passing (Item 60). Now for the fun part: converting your JavaScript to TypeScript. But where to begin?
当您向模块添加类型时,很可能会在所有依赖它的模块中出现新的类型错误。理想情况下,您希望将每个模块转换一次并完成。这意味着您应该转换依赖关系图中向上的模块:从叶子(不依赖于其他模块的模块)开始并向上移动到根。
When you add types to a module, it’s likely to surface new type errors in all the modules that depend on it. Ideally you’d like to convert each module once and be done with it. This implies that you should convert modules going up the dependency graph: starting with the leaves (modules that depend on no others) and moving up to the root.
最先迁移的模块是您的第三方依赖项,因为根据定义,您依赖于它们,但它们不依赖于您。通常这意味着安装@types模块。lodash例如,如果您使用实用程序库,您将运行npm install --save-dev @types/lodash. 这些类型将帮助类型在您的代码中流动,并在您使用库时解决问题。
The very first modules to migrate are your third-party dependencies since, by definition, you depend on them but they do not depend on you. Usually this means installing @types modules. If you use the lodash utility library, for example, you’d run npm install --save-dev @types/lodash. These typings will help types flow through your code and surface issues in your use of the libraries.
如果您的代码调用外部 API,您可能还希望尽早为这些 API 添加类型声明。尽管这些调用可能发生在您代码中的任何位置,但这仍然符合向上移动依赖关系图的精神,因为您依赖 API,但它们不依赖您。许多类型来自 API 调用,而这些通常很难从上下文中推断出来。如果你能找到 API 的规范,从中生成类型(见条款 35)。
If your code calls external APIs, you may also want to add type declarations for these early on. Although these calls may happen anywhere in your code, this is still in the spirit of moving up the dependency graph since you depend on the APIs but they do not depend on you. Many types flow from API calls, and these are generally difficult to infer from context. If you can find a spec for the API, generate types from that (see Item 35).
当您迁移自己的模块时,可视化依赖关系图会很有帮助。图 8-3显示了一个中型 JavaScript 项目的示例图,使用优秀的madge工具制作。
As you migrate your own modules, it’s helpful to visualize the dependency graph. Figure 8-3 shows an example graph from a medium-sized JavaScript project, made using the excellent madge tool.
这个依赖图的底部是utils.js和tickers.js之间的循环依赖。有很多模块依赖于这两者,但它们只相互依赖。这种模式很常见:大多数项目在依赖图的底部都有某种实用模块。
The bottom of this dependency graph is the circular dependency between utils.js and tickers.js. There are many modules that depend on these two, but they only depend on one another. This pattern is quite common: most projects will have some sort of utility module at the bottom of the dependency graph.
在迁移代码时,请专注于添加类型而不是重构。如果这是一个旧项目,您可能会注意到一些奇怪的事情并想要修复它们。抵制这种冲动!近期目标是将您的项目转换为 TypeScript,而不是改进其设计。相反,当你检测到代码味道时,写下它们并列出未来的重构。
As you migrate your code, focus on adding types rather than refactoring. If this is an old project, you’re likely to notice some strange things and want to fix them. Resist this urge! The immediate goal is to convert your project to TypeScript, not to improve its design. Instead, write down code smells as you detect them and make a list of future refactors.
在转换为 TypeScript 时,您会遇到一些常见错误。其中一些包含在项目 59中,但新的包括:
There are a few common errors you’ll run into as you convert to TypeScript. Some of these were covered in Item 59, but new ones include:
JavaScript 中的类不需要声明其成员,但 TypeScript 中的类需要。当您将类的.js文件重命名为.ts时,它可能会为您引用的每个属性显示错误:
Classes in JavaScript do not need to declare their members, but classes in TypeScript do. When you rename a class’s .js file to .ts, it’s likely to show errors for every single property you reference:
classGreeting{constructor(name){this.greeting='Hello';// ~~~~~~~~ Property 'greeting' does not exist on type 'Greeting'this.name=name;// ~~~~ Property 'name' does not exist on type 'Greeting'}greet() {returnthis.greeting+' '+this.name;// ~~~~~~~~ ~~~~ Property ... does not exist}}
classGreeting{constructor(name){this.greeting='Hello';// ~~~~~~~~ Property 'greeting' does not exist on type 'Greeting'this.name=name;// ~~~~ Property 'name' does not exist on type 'Greeting'}greet() {returnthis.greeting+' '+this.name;// ~~~~~~~~ ~~~~ Property ... does not exist}}
有一个有用的快速修复方法(参见图 8-4),您应该利用它。
There’s a helpful quick fix (see Figure 8-4) for this that you should take advantage of.
这将根据用法为缺少的成员添加声明:
This will add declarations for the missing members based on usage:
classGreeting{greeting:string;name:any;constructor(name){this.greeting='Hello';this.name=name;}greet() {returnthis.greeting+' '+this.name;}}
classGreeting{greeting:string;name:any;constructor(name){this.greeting='Hello';this.name=name;}greet() {returnthis.greeting+' '+this.name;}}
TypeScript 能够获取greeting正确的类型,但不能获取name. 应用此快速修复后,您应该查看属性列表并修复any类型。
TypeScript was able to get the type for greeting correct, but not the type for name. After applying this quick fix, you should look through the property list and fix the any types.
如果这是您第一次看到您班级的完整属性列表,您可能会感到震惊。当我转换dygraph.js中的主类(图8-3中的根模块)时,我发现它有不少于45个成员变量!迁移到 TypeScript 有一种方法可以让以前隐含的像这样的糟糕设计浮出水面。如果您必须查看它,就很难为糟糕的设计辩护。但同样,抵制现在重构的冲动。注意奇怪的地方,想想改天你会如何解决它。
If this is the first time you’ve seen the full property list for your class, you may be in for a shock. When I converted the main class in dygraph.js (the root module in Figure 8-3), I discovered that it had no fewer than 45 member variables! Migrating to TypeScript has a way of surfacing bad designs like this that were previously implicit. It’s harder to justify a bad design if you have to look at it. But again, resist the urge to refactor now. Note the oddity and think about how you’d fix it some other day.
TypeScript 会抱怨这样的代码:
TypeScript will complain about code like this:
conststate={};state.name='New York';// ~~~~ Property 'name' does not exist on type '{}'state.capital='Albany';// ~~~~~~~ Property 'capital' does not exist on type '{}'
conststate={};state.name='New York';// ~~~~ Property 'name' does not exist on type '{}'state.capital='Albany';// ~~~~~~~ Property 'capital' does not exist on type '{}'
这个主题在项目 23中有更深入的介绍,所以如果你遇到这个错误,你可能想重温一下那个项目。如果修复是微不足道的,您可以一次构建所有对象:
This topic is covered in more depth in Item 23, so you may want to brush up on that item if you run into this error. If the fix is trivial, you can build the object all at once:
conststate={name:'New York',capital:'Albany',};// OK
conststate={name:'New York',capital:'Albany',};// OK
如果不是,那么这是使用类型断言的合适时机:
If it is not, then this is an appropriate time to use a type assertion:
interfaceState{name:string;capital:string;}conststate={}asState;state.name='New York';// OKstate.capital='Albany';// OK
interfaceState{name:string;capital:string;}conststate={}asState;state.name='New York';// OKstate.capital='Albany';// OK
您应该最终解决这个问题(请参阅第 9 项),但这是权宜之计,将帮助您继续迁移。
You should fix this eventually (see Item 9), but this is expedient and will help you keep the migration going.
如果您一直在使用 JSDoc 和@ts-check(Item 59),请注意您实际上可以通过转换为 TypeScript 来失去类型安全。例如,TypeScript 会在这段 JavaScript 中标记错误:
If you’ve been using JSDoc and @ts-check (Item 59), be aware that you can actually lose type safety by converting to TypeScript. For instance, TypeScript flags an error in this JavaScript:
// @ts-check/*** @param {number} num*/functiondouble(num){return2*num;}double('trouble');// ~~~~~~~~~ Argument of type '"trouble"' is not assignable to// parameter of type 'number'
// @ts-check/*** @param {number} num*/functiondouble(num){return2*num;}double('trouble');// ~~~~~~~~~ Argument of type '"trouble"' is not assignable to// parameter of type 'number'
当您转换为 TypeScript 时,JSDoc@ts-check和 JSDoc 将停止执行。这意味着的类型num是 implicitly any,所以没有错误:
When you convert to TypeScript, the @ts-check and JSDoc stop being enforced. This means the type of num is implicitly any, so there’s no error:
/*** @param {number} num*/functiondouble(num){return2*num;}double('trouble');// OK
/*** @param {number} num*/functiondouble(num){return2*num;}double('trouble');// OK
幸运的是,有一个快速修复可用于将 JSDoc 类型移动到 TypeScript 类型。如果您有任何 JSDoc,则应该使用图 8-5中所示的内容。
Fortunately there’s a quick fix available to move JSDoc types to TypeScript types. If you have any JSDoc, you should use what’s shown in Figure 8-5.
将类型注释复制到 TypeScript 后,请确保将它们从 JSDoc 中删除以避免冗余(请参阅第 30 项):
Once you’ve copied type annotations to TypeScript, make sure to remove them from the JSDoc to avoid redundancy (see Item 30):
functiondouble(num:number){return2*num;}double('trouble');// ~~~~~~~~~ Argument of type '"trouble"' is not assignable to// parameter of type 'number'
functiondouble(num:number){return2*num;}double('trouble');// ~~~~~~~~~ Argument of type '"trouble"' is not assignable to// parameter of type 'number'
当您打开时也会遇到此问题noImplicitAny,但您最好现在添加类型。
This issue will also be caught when you turn on noImplicitAny, but you may as well add the types now.
最后迁移你的测试。它们应该位于您的依赖关系图的顶部(因为您的代码不依赖于它们),并且尽管您根本没有更改它们,但知道您的测试在迁移过程中继续通过是非常有帮助的。
Migrate your tests last. They should be at the top of your dependency graph (since your code doesn’t depend on them), and it’s extremely helpful to know that your tests continue to pass during the migration despite your not having changed them at all.
@types通过添加第三方模块和外部 API 调用来开始迁移。
Start migration by adding @types for third-party modules and external API calls.
开始从依赖关系图的底部向上迁移您的模块。第一个模块通常是某种实用程序代码。考虑可视化依赖关系图以帮助您跟踪进度。
Begin migrating your modules from the bottom of the dependency graph upwards. The first module will usually be some sort of utility code. Consider visualizing the dependency graph to help you track progress.
在发现奇怪的设计时,要克制重构代码的冲动。为未来的重构保留一个想法列表,但要专注于 TypeScript 转换。
Resist the urge to refactor your code as you uncover odd designs. Keep a list of ideas for future refactors, but stay focused on TypeScript conversion.
Be aware of common errors that come up during conversion. Copy JSDoc annotations if necessary to avoid losing type safety as you convert.
转换您对.ts 的整个项目是一项巨大的成就。但是你的工作还没有完成。您的下一个目标是打开选项noImplicitAny(项目 2)。没有 TypeScript 代码noImplicitAny最好被认为是过渡性的,因为它可以掩盖你在类型声明中犯的真正错误。
Converting your whole project to .ts is a big accomplishment. But your work isn’t done quite yet. Your next goal is to turn on the noImplicitAny option (Item 2). TypeScript code without noImplicitAny is best thought of as transitional because it can mask real errors you’ve made in your type declarations.
例如,您可能已经使用“添加所有缺少的成员”快速修复来向类添加属性声明(条目 61)。您只剩下一个any类型并想修复它:
For example, perhaps you’ve used the “Add all missing members” quick fix to add property declarations to a class (Item 61). You’re left with an any type and would like to fix it:
classChart{indices:any;// ...}
classChart{indices:any;// ...}
indices听起来它应该是一个数字数组,所以你插入那个类型:
indices sounds like it should be an array of numbers, so you plug in that type:
classChart{indices:number[];// ...}
classChart{indices:number[];// ...}
没有新的错误结果,所以你继续前进。不幸的是,你犯了一个错误:number[]是错误的类型。这是课堂上其他地方的一些代码:
No new errors result, so you then keep moving. Unfortunately, you’ve made a mistake: number[] is the wrong type. Here’s some code from elsewhere in the class:
getRanges() {for(constrofthis.indices){constlow=r[0];// Type is anyconsthigh=r[1];// Type is any// ...}}
getRanges() {for(constrofthis.indices){constlow=r[0];// Type is anyconsthigh=r[1];// Type is any// ...}}
显然number[][]或[number, number][]将是更准确的类型。number允许对 a 进行索引会让您感到惊讶吗?将此视为没有noImplicitAny.
Clearly number[][] or [number, number][] would be a more accurate type. Does it surprise you that indexing into a number is allowed? Take this as an indication of just how loose TypeScript can be without noImplicitAny.
当你打开时noImplicitAny,这变成了一个错误:
When you turn on noImplicitAny, this becomes an error:
getRanges() {for(constrofthis.indices){constlow=r[0];// ~~~~ Element implicitly has an 'any' type because// type 'Number' has no index signatureconsthigh=r[1];// ~~~~ Element implicitly has an 'any' type because// type 'Number' has no index signature// ...}}
getRanges() {for(constrofthis.indices){constlow=r[0];// ~~~~ Element implicitly has an 'any' type because// type 'Number' has no index signatureconsthigh=r[1];// ~~~~ Element implicitly has an 'any' type because// type 'Number' has no index signature// ...}}
A启用的好策略noImplicitAny是在您的本地客户端中设置它并开始修复错误。从类型检查器中得到的错误数量可以让您很好地了解自己的进度。您可以在不提交tsconfig.json更改的情况下提交类型更正,直到将错误数减少到零。
A good strategy for enabling noImplicitAny is to set it in your local client and start fixing errors. The number of errors you get from the type checker gives you a good sense of your progress. You can commit the type corrections without committing the tsconfig.json change until you get the number of errors down to zero.
您可以使用许多其他旋钮来增加类型检查的严格性,最终以"strict": true. 但这noImplicitAny是最重要的一个,你的项目将获得 TypeScript 的大部分好处,即使你不采用其他设置,如strictNullChecks. 在采用更严格的设置之前,让团队中的每个人都有机会习惯 TypeScript。
There are many other knobs you can turn to increase the strictness of type checking, culminating with "strict": true. But noImplicitAny is the most important one and your project will get most of the benefits of TypeScript even if you don’t adopt other settings like strictNullChecks. Give everyone on your team a chance to get used to TypeScript before you adopt stricter settings.
在您采用noImplicitAny. 松散的类型检查可以掩盖类型声明中的真正错误。
Don’t consider your TypeScript migration done until you adopt noImplicitAny. Loose type checking can mask real mistakes in type declarations.
在强制执行之前逐渐修复类型错误noImplicitAny。在采用更严格的检查之前,让您的团队有机会熟悉 TypeScript。
Fix type errors gradually before enforcing noImplicitAny. Give your team a chance to get comfortable with TypeScript before adopting stricter checks.
1个Z. Gao、C. Bird 和 ET Barr,“打字还是不打字:量化 JavaScript 中的可检测错误”,ICSE 2017,http: //earlbarr.com/publications/typestudy.pdf。
1 Z. Gao, C. Bird, and E. T. Barr, “To Type or Not to Type: Quantifying Detectable Bugs in JavaScript,” ICSE 2017, http://earlbarr.com/publications/typestudy.pdf.
2个Brie Bunge,“大规模采用 TypeScript”,JSConf Hawaii 2019,https://youtu.be/P-J9Eg7hJwE。
2 Brie Bunge, “Adopting TypeScript at Scale,” JSConf Hawaii 2019, https://youtu.be/P-J9Eg7hJwE.
Effective TypeScript封面上的动物是红嘴牛啄木鸟 ( Buphagus erythrorhynchus )。这些鸟类栖息在东非的分散范围内,从东北部的埃塞俄比亚和索马里到南非的几个小岛;然而,可以说这些鸟类栖息在它们几乎一生都在其上度过的食草动物的范围内。
The animal on the cover of Effective TypeScript is a red-billed oxpecker (Buphagus erythrorhynchus). These birds inhabit a fragmented range across eastern Africa, from Ethiopia and Somalia in the northeast to a few pockets in South Africa; however, these birds can be said to inhabit the range of the grazing animals on which they spend almost all their lives.
红嘴啄木鸟与八哥和八哥有亲缘关系,尽管它们属于不同的独立科。这些鸟长约八英寸,重约两盎司,头部、背部和尾巴呈树皮棕色,下方颜色较浅。它们最显着的身体特征是红色的喙和红色的眼睛,衬托着明亮的黄色眼环。
Red-billed oxpeckers are related to starlings and mynahs, though they are of a distinct and separate family. About eight inches long, and weighing about two ounces, these birds have a bark-brown head, back, and tail, with paler coloring below. Their most striking physical features are their red beaks and red eyes set off by bright yellow eyerings.
决定这只鸟生活的是它在何处以及如何找到食物:红嘴啄木鸟以蜱虫和其他动物寄生虫为食,并在觅食时栖息在动物身上。它们的宿主动物通常是羚羊(如捻角羚和黑斑羚)以及大型动物,如斑马、长颈鹿、水牛和犀牛(大象不能容忍它们)。红嘴啄木鸟已经进化出适应性以帮助它们寻找食物,例如可以刺穿厚厚的动物毛发的扁平喙,以及可以挂在宿主动物身上的锋利的爪子和坚硬的尾巴。这些鸟甚至在栖息在宿主动物身上时进行求偶,并且只在筑巢季节离开。父母鸟在靠近动物群的巢穴(内衬从宿主身上拔下的毛发)中养育三只小鸡,以便它们可以养活自己和幼崽。
Dominating the life of this bird is where and how it finds its food: red-billed oxpeckers feed on ticks and other animal parasites, and they perch on animals as they forage. Their host animals are most often antelope (such as kudu and impala) as well as large animals such as zebra, giraffe, buffalo, and rhinoceros (elephants do not tolerate them). Red-billed oxpeckers have evolved adaptations to assist them in their search for food, such as a flat beak to pierce thick animal hair, and sharp claws and a stiff tail to hang on to their host animals. These birds even conduct courtship while perched on a host animal, and only leave during nesting season. Parent birds raise three chicks in a nest hole (lined with hair pulled from their host) close to the animal herds so that they can feed themselves and their young.
鸟类与其动物宿主的关系曾被视为互利共生(物种之间互惠互利的相互作用)的一个明确而经典的例子。然而,最近的研究表明,啄牛鸟的进食习惯不会显着影响宿主的寄生虫数量,而且还表明,啄牛鸟实际上可以保持动物的伤口开放,以便它们可以吸食它们的血液。
The birds’ relationship with their animal hosts was once seen as a clear-cut and classic example of mutualism (a mutually beneficial interaction between species). However, recent studies have shown that oxpeckers’ feeding habits don’t significantly affect hosts’ parasite loads, and additionally showed that oxpeckers actually work to keep animals’ wounds open, so that they can feed on their blood.
红嘴啄木鸟在其分布范围内仍然很常见;尽管使用杀虫剂是一种威胁,但他们采用家养牛群作为食物来源有助于他们的人口保持稳定。O'Reilly 封面上的许多动物都濒临灭绝;所有这些对世界都很重要。
Red-billed oxpeckers remain common across their range; though pesticide use is a threat, their adoption of domestic cattle herds as a food source helps their population remain stable. Many of the animals on O’Reilly covers are endangered; all of them are important to the world.
封面插图由 Jose Marzan 绘制,基于Elements of Ornithology 的黑白版画。封面字体是 Gilroy 和 Guardian Sans。文字字体为Adobe Minion Pro;标题字体为 Adobe Myriad Condensed;代码字体是 Dalton Maag 的 Ubuntu Mono。
The cover illustration is by Jose Marzan, based on a black-and-white engraving from Elements of Ornithology. The cover fonts are Gilroy and Guardian Sans. The text font is Adobe Minion Pro; the heading font is Adobe Myriad Condensed; and the code font is Dalton Maag’s Ubuntu Mono.